@uniai-fe/uds-templates 0.1.20 → 0.1.22

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 (79) hide show
  1. package/dist/styles.css +2 -2
  2. package/package.json +8 -4
  3. package/src/auth/login/jotai/user.ts +13 -0
  4. package/src/auth/login/types/api.ts +229 -0
  5. package/src/auth/login/types/form.ts +1 -0
  6. package/src/auth/login/types/index.ts +4 -0
  7. package/src/auth/signup/markup/VerificationForm.tsx +3 -2
  8. package/src/cctv/apis/client.ts +61 -0
  9. package/src/cctv/apis/index.ts +2 -0
  10. package/src/cctv/apis/server.ts +188 -0
  11. package/src/cctv/components/Provider.tsx +47 -0
  12. package/src/cctv/components/__viewer.tsx +99 -0
  13. package/src/cctv/components/cam-list/Container.tsx +36 -0
  14. package/src/cctv/components/cam-list/Item.tsx +71 -0
  15. package/src/cctv/components/cam-list/index.tsx +7 -0
  16. package/src/cctv/components/index.tsx +13 -0
  17. package/src/cctv/components/pagination/Container.tsx +26 -0
  18. package/src/cctv/components/pagination/Control.tsx +29 -0
  19. package/src/cctv/components/pagination/Provider.tsx +204 -0
  20. package/src/cctv/components/pagination/buttons/Base.tsx +56 -0
  21. package/src/cctv/components/pagination/buttons/Next.tsx +34 -0
  22. package/src/cctv/components/pagination/buttons/Prev.tsx +34 -0
  23. package/src/cctv/components/pagination/index.tsx +25 -0
  24. package/src/cctv/components/pagination/list/Carousel.tsx +26 -0
  25. package/src/cctv/components/pagination/list/Container.tsx +30 -0
  26. package/src/cctv/components/pagination/list/Item.tsx +81 -0
  27. package/src/cctv/components/video/Container.tsx +13 -0
  28. package/src/cctv/components/video/Video.tsx +34 -0
  29. package/src/cctv/components/video/index.tsx +9 -0
  30. package/src/cctv/components/video/overlay/Container.tsx +15 -0
  31. package/src/cctv/components/video/overlay/Title.tsx +28 -0
  32. package/src/cctv/components/video/overlay/body/Container.tsx +13 -0
  33. package/src/cctv/components/video/overlay/body/Error.tsx +16 -0
  34. package/src/cctv/components/video/overlay/footer/Container.tsx +30 -0
  35. package/src/cctv/components/video/overlay/footer/OpenButton.tsx +19 -0
  36. package/src/cctv/components/video/overlay/header/CloseButton.tsx +14 -0
  37. package/src/cctv/components/video/overlay/header/Container.tsx +50 -0
  38. package/src/cctv/components/video/overlay/header/LiveState.tsx +21 -0
  39. package/src/cctv/components/video/overlay/index.tsx +24 -0
  40. package/src/cctv/components/viewer/Container.tsx +13 -0
  41. package/src/cctv/components/viewer/desktop/Container.tsx +38 -0
  42. package/src/cctv/components/viewer/desktop/Pagination.tsx +20 -0
  43. package/src/cctv/components/viewer/desktop/Placeholder.tsx +18 -0
  44. package/src/cctv/components/viewer/desktop/Video.tsx +83 -0
  45. package/src/cctv/components/viewer/index.tsx +12 -0
  46. package/src/cctv/components/viewer/mobile/Container.tsx +13 -0
  47. package/src/cctv/data/context.ts +22 -0
  48. package/src/cctv/data/index.ts +1 -0
  49. package/src/cctv/hooks/index.tsx +5 -0
  50. package/src/cctv/hooks/useCompanyData.tsx +39 -0
  51. package/src/cctv/hooks/useContext.ts +150 -0
  52. package/src/cctv/hooks/useRtcStream.ts +94 -0
  53. package/src/cctv/img/chevron-left.svg +3 -0
  54. package/src/cctv/img/chevron-right.svg +3 -0
  55. package/src/cctv/img/error.svg +4 -0
  56. package/src/cctv/img/viewer-close.svg +3 -0
  57. package/src/cctv/img/viewer-open.svg +6 -0
  58. package/src/cctv/index.scss +1 -0
  59. package/src/cctv/index.tsx +9 -0
  60. package/src/cctv/jotai/context.ts +9 -0
  61. package/src/cctv/jotai/index.ts +1 -0
  62. package/src/cctv/styles/cam-list.scss +32 -0
  63. package/src/cctv/styles/index.scss +5 -0
  64. package/src/cctv/styles/pagination.scss +77 -0
  65. package/src/cctv/styles/variables.scss +38 -0
  66. package/src/cctv/styles/video.scss +142 -0
  67. package/src/cctv/styles/viewer.scss +7 -0
  68. package/src/cctv/types/api.ts +166 -0
  69. package/src/cctv/types/carousel.ts +24 -0
  70. package/src/cctv/types/context.ts +68 -0
  71. package/src/cctv/types/index.ts +4 -0
  72. package/src/cctv/types/list.ts +94 -0
  73. package/src/cctv/utils/data.ts +40 -0
  74. package/src/cctv/utils/select.ts +62 -0
  75. package/src/index.tsx +3 -0
  76. package/src/modal/styles/base.scss +2 -2
  77. package/src/types/api.ts +43 -0
  78. package/src/types/index.ts +1 -0
  79. package/src/auth/login/types.ts +0 -2
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo } from "react";
4
+ import { useFormContext } from "react-hook-form";
5
+ import { useQueryCctvCompanyList } from "../apis";
6
+ import { useCctvEndpoints } from "../components/Provider";
7
+ import type { CctvContext } from "../types";
8
+
9
+ export default function useCctvCompanyData({
10
+ username,
11
+ url,
12
+ }: {
13
+ username: string;
14
+ url?: string;
15
+ }) {
16
+ const { listUrl } = useCctvEndpoints();
17
+ const resolvedUrl = useMemo(() => url ?? listUrl, [url, listUrl]);
18
+ const { setValue } = useFormContext<CctvContext>();
19
+ const { data, isFetching, isError, ...rest } = useQueryCctvCompanyList({
20
+ username,
21
+ url: resolvedUrl,
22
+ });
23
+
24
+ useEffect(() => {
25
+ if (data?.data) {
26
+ setValue("rawData", data.data);
27
+ }
28
+ }, [data, setValue]);
29
+
30
+ useEffect(() => {
31
+ setValue("isFetching", Boolean(isFetching));
32
+ }, [isFetching, setValue]);
33
+
34
+ useEffect(() => {
35
+ setValue("isError", Boolean(isError));
36
+ }, [isError, setValue]);
37
+
38
+ return { data, isFetching, isError, ...rest };
39
+ }
@@ -0,0 +1,150 @@
1
+ "use client";
2
+
3
+ import { useCallback, useMemo } from "react";
4
+ import { useSetAtom } from "jotai";
5
+ import { useFormContext, useWatch } from "react-hook-form";
6
+
7
+ import { cctvSelectedContext } from "../jotai/context";
8
+ import type {
9
+ CctvCompanyCameraList,
10
+ CctvCompanyGroup,
11
+ CctvCompanyGroupItems,
12
+ CctvContext,
13
+ } from "../types";
14
+ import { initCompanyList } from "../utils/data";
15
+ import { getCam, getCamList, getCompany } from "../utils/select";
16
+
17
+ export default function useCctvContext(params?: { pageCode?: string }) {
18
+ const { pageCode } = params ?? {};
19
+
20
+ // const username = useCctvUsername();
21
+ const setCtx = useSetAtom(cctvSelectedContext);
22
+ const { control, setValue, ...context } = useFormContext<CctvContext>();
23
+
24
+ const rawData = useWatch({ control, name: "rawData" });
25
+ // console.log("useCctvContext rawData:", rawData);
26
+ const isFetching = useWatch({ control, name: "isFetching" });
27
+ const isError = useWatch({ control, name: "isError" });
28
+
29
+ const companyList = useMemo(
30
+ () => initCompanyList({ data: rawData, pageCode }),
31
+ [rawData, pageCode],
32
+ );
33
+
34
+ // console.log("useCctvContext companyList:", companyList);
35
+
36
+ const searchedKeyword = useWatch({ control, name: "filter" });
37
+ const selectedCompanyId = useWatch({ control, name: "company_id" });
38
+ const selectedCamId = useWatch({ control, name: "cam_id" });
39
+
40
+ const onSelectCompany = useCallback(
41
+ (company_id: string) => {
42
+ if (selectedCompanyId === company_id) return;
43
+
44
+ setValue("company_id", company_id);
45
+ setValue("cam_id", "");
46
+
47
+ setCtx({ company_id, cam_id: "" });
48
+ },
49
+ [selectedCompanyId, setValue, setCtx],
50
+ );
51
+
52
+ const onToggleCamera = useCallback(
53
+ (cam_id: string) => {
54
+ setValue("cam_id", selectedCamId === cam_id ? "" : cam_id);
55
+ setCtx(prev => ({
56
+ ...prev,
57
+ cam_id: selectedCamId === cam_id ? "" : cam_id,
58
+ }));
59
+ },
60
+ [selectedCamId, setValue, setCtx],
61
+ );
62
+
63
+ const onOpenCamera = useCallback(
64
+ (cam_id: string) => {
65
+ if (selectedCamId === cam_id) return;
66
+ setValue("cam_id", cam_id);
67
+ setCtx(prev => ({ ...prev, cam_id: cam_id }));
68
+ },
69
+ [selectedCamId, setValue, setCtx],
70
+ );
71
+ const onCloseCamera = useCallback(
72
+ (cam_id: string) => {
73
+ if (selectedCamId !== cam_id) return;
74
+ setValue("cam_id", "");
75
+ setCtx(prev => ({ ...prev, cam_id: "" }));
76
+ },
77
+ [selectedCamId, setValue, setCtx],
78
+ );
79
+
80
+ const groups = useMemo(
81
+ (): CctvCompanyGroup[] =>
82
+ companyList.map(group => ({
83
+ ...group,
84
+ list: group.list.map((d: CctvCompanyGroupItems) => ({
85
+ ...d,
86
+ selected: d.company_id === selectedCompanyId,
87
+ onSelect: () => onSelectCompany(d.company_id),
88
+ cam_list: d.cam_list.map((cam: CctvCompanyCameraList) => ({
89
+ ...cam,
90
+ selected: cam.cam_id === selectedCamId,
91
+ onSelect: () => onOpenCamera(cam.cam_id),
92
+ })),
93
+ })),
94
+ })),
95
+ [
96
+ companyList,
97
+ onSelectCompany,
98
+ onOpenCamera,
99
+ selectedCamId,
100
+ selectedCompanyId,
101
+ ],
102
+ );
103
+
104
+ const selectedCompany = useMemo(
105
+ (): CctvCompanyGroupItems | undefined =>
106
+ getCompany(groups, selectedCompanyId),
107
+ [selectedCompanyId, groups],
108
+ );
109
+
110
+ const cams = useMemo(
111
+ (): CctvCompanyCameraList[] => getCamList(groups, selectedCompanyId),
112
+ [selectedCompanyId, groups],
113
+ );
114
+
115
+ const selectedCam = useMemo(
116
+ (): Omit<CctvCompanyCameraList, "renderKey"> | undefined =>
117
+ getCam(groups, selectedCompanyId, selectedCamId),
118
+ [selectedCamId, selectedCompanyId, groups],
119
+ );
120
+
121
+ const companyValidList = useMemo(
122
+ () =>
123
+ groups
124
+ .flatMap(({ list }) => list)
125
+ .filter(
126
+ ({ cam_list }) => !cam_list.every(({ cam_online }) => !cam_online),
127
+ )
128
+ .map(({ company_id }) => company_id),
129
+ [groups],
130
+ );
131
+
132
+ return {
133
+ isFetching,
134
+ isError,
135
+ searchedKeyword,
136
+ selectedCompanyId,
137
+ selectedCamId,
138
+ selectedCompany,
139
+ selectedCam,
140
+ groups,
141
+ companyValidList,
142
+ cams,
143
+ control,
144
+ setValue,
145
+ ...context,
146
+ onToggleCamera,
147
+ onOpenCamera,
148
+ onCloseCamera,
149
+ };
150
+ }
@@ -0,0 +1,94 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+ import { startWhepStream, type WhepStreamHandle } from "@uniai-fe/util-rtc";
5
+
6
+ import { useQueryCctvRtcToken } from "../apis/client";
7
+ import { useCctvEndpoints } from "../components/Provider";
8
+ import type { CctvCompanyCameraList } from "../types";
9
+ import { useFormContext, useWatch } from "react-hook-form";
10
+
11
+ interface UseCctvRtcStreamParams {
12
+ cam?: Omit<CctvCompanyCameraList, "renderKey">;
13
+ companyId?: string;
14
+ tokenUrl?: string;
15
+ }
16
+
17
+ export function useCctvRtcStream({
18
+ cam,
19
+ companyId,
20
+ tokenUrl,
21
+ }: UseCctvRtcStreamParams) {
22
+ const { tokenUrl: contextTokenUrl } = useCctvEndpoints();
23
+ const videoRef = useRef<HTMLVideoElement | null>(null);
24
+ const [connectionState, setConnectionState] =
25
+ useState<RTCPeerConnectionState>("new");
26
+ const [streamError, setStreamError] = useState<string | null>(null);
27
+ const [isStreaming, setStreaming] = useState(false);
28
+
29
+ const { control } = useFormContext();
30
+ const username = useWatch({ control, name: "username" });
31
+
32
+ const tokenQuery = useQueryCctvRtcToken({
33
+ company_id: companyId ?? "",
34
+ cam_id: cam?.cam_id ?? "",
35
+ username,
36
+ url: tokenUrl ?? contextTokenUrl,
37
+ });
38
+
39
+ const endpoint = useMemo(() => {
40
+ if (!cam?.cam_rtc) return undefined;
41
+ const query = username ? `?username=${encodeURIComponent(username)}` : "";
42
+ return `${cam.cam_rtc.replace(/\/$/, "")}/whep${query}`;
43
+ }, [cam?.cam_rtc, username]);
44
+
45
+ useEffect(() => {
46
+ const currentVideo = videoRef.current;
47
+ // 현재 렌더에서 할당된 video 요소를 캡처해 cleanup 시 안전하게 참조한다.
48
+ if (!tokenQuery.data?.token || !endpoint || !currentVideo) return;
49
+
50
+ let handle: WhepStreamHandle | null = null;
51
+ const controller = new AbortController();
52
+ setStreamError(null);
53
+ setStreaming(true);
54
+ setConnectionState("connecting");
55
+
56
+ startWhepStream({
57
+ endpoint,
58
+ token: tokenQuery.data.token,
59
+ video: currentVideo,
60
+ signal: controller.signal,
61
+ onConnectionStateChange: state => setConnectionState(state),
62
+ })
63
+ .then(streamHandle => {
64
+ handle = streamHandle;
65
+ })
66
+ .catch(error => {
67
+ setStreamError(
68
+ error instanceof Error
69
+ ? error.message
70
+ : "스트림 연결에 실패했습니다.",
71
+ );
72
+ })
73
+ .finally(() => {
74
+ setStreaming(false);
75
+ });
76
+
77
+ return () => {
78
+ controller.abort();
79
+ handle?.close();
80
+ handle = null;
81
+ currentVideo.srcObject = null;
82
+ };
83
+ }, [endpoint, tokenQuery.data?.token, cam?.cam_id]);
84
+
85
+ return {
86
+ videoRef,
87
+ connectionState,
88
+ streamError,
89
+ isStreaming,
90
+ isTokenLoading: tokenQuery.isFetching,
91
+ isTokenError: tokenQuery.isError,
92
+ refetchToken: tokenQuery.refetch,
93
+ };
94
+ }
@@ -0,0 +1,3 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M15 4L7 12L15 20" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M9 4L17 12L9 20" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
3
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M1.43463 4.43463C1.74705 4.12221 2.25307 4.12221 2.56549 4.43463L16.5655 18.4346C16.8779 18.7471 16.8779 19.2531 16.5655 19.5655C16.2531 19.8779 15.7471 19.8778 15.4346 19.5655L14.6016 18.7325C14.4079 18.7757 14.2069 18.7999 14.0001 18.7999H5.00006C3.45368 18.7999 2.20029 17.5464 2.20026 16.0001V8.00006C2.20026 7.51792 2.32348 7.06501 2.53815 6.66901L1.43463 5.56549C1.12221 5.25307 1.12221 4.74705 1.43463 4.43463Z" fill="#CACBCE"/>
3
+ <path d="M14.0001 5.20026C15.5464 5.20029 16.7999 6.45368 16.7999 8.00006V8.81744L19.3311 7.80573C20.5133 7.33288 21.7995 8.20346 21.7999 9.47662V14.5235C21.7995 15.7966 20.5133 16.6672 19.3311 16.1944L16.7999 15.1817V16.0001C16.7999 16.4067 16.7106 16.792 16.5547 17.1407L4.63971 5.22565C4.75777 5.21047 4.87787 5.20026 5.00006 5.20026H14.0001Z" fill="#CACBCE"/>
4
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M24.5009 9.65186C24.9695 9.18323 25.7285 9.18323 26.1972 9.65186C26.6658 10.1205 26.6658 10.8795 26.1972 11.3481L19.62 17.9238L26.1972 24.501C26.6658 24.9696 26.6658 25.7286 26.1972 26.1973C25.7285 26.6659 24.9695 26.6659 24.5009 26.1973L17.9237 19.6201L11.3481 26.1973C10.8794 26.6659 10.1204 26.6659 9.65176 26.1973C9.18314 25.7286 9.18314 24.9696 9.65176 24.501L16.2274 17.9238L9.65176 11.3481C9.18314 10.8795 9.18314 10.1205 9.65176 9.65186C10.1204 9.18323 10.8794 9.18323 11.3481 9.65186L17.9237 16.2275L24.5009 9.65186Z" fill="white"/>
3
+ </svg>
@@ -0,0 +1,6 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M4 9.5V6.5C4 5.39543 4.89543 4.5 6 4.5H9" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
3
+ <path d="M15 4.5L18 4.5C19.1046 4.5 20 5.39543 20 6.5L20 9.5" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
4
+ <path d="M9 19.5L6 19.5C4.89543 19.5 4 18.6046 4 17.5L4 14.5" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
5
+ <path d="M20 14.5L20 17.5C20 18.6046 19.1046 19.5 18 19.5L15 19.5" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
6
+ </svg>
@@ -0,0 +1 @@
1
+ @use "./styles/index.scss";
@@ -0,0 +1,9 @@
1
+ import "./index.scss";
2
+
3
+ export * from "./apis";
4
+ export * from "./data";
5
+ export * from "./components";
6
+ export * from "./hooks";
7
+ export * from "./jotai";
8
+
9
+ export type * from "./types";
@@ -0,0 +1,9 @@
1
+ "use client";
2
+
3
+ import { atom } from "jotai";
4
+ import type { CctvSelectedContext } from "../types";
5
+
6
+ export const cctvSelectedContext = atom<CctvSelectedContext>({
7
+ company_id: "",
8
+ cam_id: "",
9
+ });
@@ -0,0 +1 @@
1
+ export * from "./context";
@@ -0,0 +1,32 @@
1
+ .cctv-cam-list-container {
2
+ width: 100%;
3
+ height: 100%;
4
+
5
+ overflow-x: hidden;
6
+ overflow-y: scroll;
7
+
8
+ scroll-snap-type: y proximity;
9
+ scrollbar-width: none;
10
+ -ms-overflow-style: none;
11
+
12
+ &::-webkit-scrollbar {
13
+ display: none;
14
+ }
15
+ }
16
+
17
+ .cctv-cam-list-track {
18
+ display: grid;
19
+ width: 100%;
20
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
21
+ gap: var(--cctv-list-gap);
22
+ padding: 0;
23
+ margin: 0;
24
+ scroll-behavior: smooth;
25
+ & > * {
26
+ scroll-snap-align: start;
27
+ }
28
+ }
29
+
30
+ .cctv-cam-list-item .cctv-video-container {
31
+ max-height: 240px;
32
+ }
@@ -0,0 +1,5 @@
1
+ @use "./variables.scss";
2
+ @use "./video.scss";
3
+ @use "./cam-list.scss";
4
+ @use "./pagination.scss";
5
+ @use "./viewer.scss";
@@ -0,0 +1,77 @@
1
+ .cctv-pagination-container {
2
+ width: 100%;
3
+ display: flex;
4
+ align-items: center;
5
+ gap: var(--cctv-carousel-gap);
6
+ }
7
+
8
+ .cctv-pagination-list-container {
9
+ width: calc(
10
+ 100% - (var(--cctv-pagination-move-button-size) * 2) -
11
+ (var(--cctv-carousel-gap) * 2)
12
+ );
13
+ }
14
+
15
+ .cctv-pagination-viewport {
16
+ overflow-x: auto;
17
+ overflow-y: hidden;
18
+ width: 100%;
19
+ scroll-snap-type: x proximity;
20
+ scrollbar-width: none;
21
+ -ms-overflow-style: none;
22
+
23
+ &::-webkit-scrollbar {
24
+ display: none;
25
+ }
26
+ }
27
+
28
+ .cctv-pagination-track {
29
+ // Carousel 트랙은 flex로 구성하고 gap으로 간격을 맞춘다.
30
+ display: flex;
31
+ gap: var(--cctv-carousel-gap);
32
+ margin: 0;
33
+ padding: 0;
34
+ list-style: none;
35
+ scroll-behavior: smooth;
36
+ & > * {
37
+ scroll-snap-align: start;
38
+ }
39
+ }
40
+
41
+ .cctv-pagination-track > li {
42
+ // panel 너비를 고정해 viewport 이동 시 균등하게 보이도록 한다.
43
+ flex: 0 0 minmax(150px, 1fr);
44
+ }
45
+
46
+ .cctv-pagination-list-item {
47
+ cursor: pointer;
48
+ }
49
+
50
+ .cctv-pagination-list-item .cctv-video-container {
51
+ max-height: 152px;
52
+ }
53
+
54
+ .cctv-pagination-list-thumbnail {
55
+ width: 100%;
56
+ height: 100%;
57
+ }
58
+
59
+ .cctv-pagination-move-button {
60
+ width: var(--cctv-pagination-move-button-size);
61
+ height: var(--cctv-pagination-move-button-size);
62
+ border-radius: var(--cctv-pagination-move-button-size);
63
+
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+
68
+ cursor: pointer;
69
+
70
+ background: var(--cctv-pagination-move-button-active);
71
+ &.disabled {
72
+ background: var(--cctv-pagination-move-button-disabled);
73
+ }
74
+ &:disabled {
75
+ background: var(--cctv-pagination-move-button-disabled);
76
+ }
77
+ }
@@ -0,0 +1,38 @@
1
+ :root {
2
+ /* Card layout */
3
+ --cctv-video-radius: 12px;
4
+ --cctv-list-gap: var(--spacing-gap-5);
5
+
6
+ --cctv-overlay-padding-x: var(--spacing-padding-8);
7
+ --cctv-overlay-padding-y: var(--spacing-padding-6);
8
+
9
+ --cctv-video-bg: #5f5f5f;
10
+ --cctv-overlay-bg: linear-gradient(
11
+ 180deg,
12
+ rgba(0, 0, 0, 0) 0%,
13
+ rgba(0, 0, 0, 0.2) 100%
14
+ );
15
+
16
+ /* Live State */
17
+ --cctv-live-state-bg-active: var(--color-cool-gray-10);
18
+ --cctv-live-state-dot-active: var(--color-feedback-new);
19
+ --cctv-live-state-text-active: var(--color-common-99);
20
+ --cctv-live-state-bg-disabled: var(--color-cool-gray-55);
21
+ --cctv-live-state-dot-disabled: var(--color-border-strong);
22
+ --cctv-live-state-text-disabled: var(--color-label-disabled);
23
+
24
+ /* Error */
25
+ --cctv-error-text-color: var(--color-label-disabled);
26
+ --cctv-error-icon-color: var(--color-label-disabled);
27
+
28
+ /* Pagination */
29
+ --cctv-carousel-gap: var(--spacing-gap-5);
30
+ --cctv-pagination-move-button-size: 48px;
31
+ --cctv-pagination-move-button-active: var(--color-cool-gray-20);
32
+ --cctv-pagination-move-button-disabled: var(--color-cool-gray-85);
33
+
34
+ /* Controls */
35
+ --cctv-open-button-bg: rgba(0, 0, 0, 0.55);
36
+ --cctv-open-button-color: #ffffff;
37
+ --cctv-close-button-bg: rgba(0, 0, 0, 0.45);
38
+ }
@@ -0,0 +1,142 @@
1
+ .cctv-video-container {
2
+ position: relative;
3
+ width: 100%;
4
+ aspect-ratio: 4 / 3;
5
+ min-height: 240px;
6
+ border-radius: var(--cctv-video-radius);
7
+
8
+ background: var(--cctv-video-bg);
9
+ // overflow: hidden;
10
+ }
11
+
12
+ .cctv-video-box {
13
+ width: 100%;
14
+ height: 100%;
15
+ }
16
+
17
+ .cctv-video-box video {
18
+ width: 100%;
19
+ height: 100%;
20
+ margin: 0;
21
+ display: block;
22
+ object-fit: contain;
23
+ }
24
+
25
+ .cctv-video-overlay-container {
26
+ padding: var(--cctv-overlay-padding-y) var(--cctv-overlay-padding-x);
27
+
28
+ position: absolute;
29
+ inset: 0;
30
+ z-index: 1;
31
+
32
+ display: flex;
33
+ flex-direction: column;
34
+
35
+ background: var(--cctv-overlay-bg);
36
+ }
37
+ .cctv-video-overlay-header {
38
+ width: 100%;
39
+ height: fit-content;
40
+ }
41
+
42
+ .cctv-video-overlay-header-upper,
43
+ .cctv-video-overlay-header-lower {
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: space-between;
47
+ }
48
+
49
+ .cctv-video-live-state {
50
+ --theme-badge-font-weight: 400;
51
+
52
+ margin-right: 4px;
53
+ }
54
+ .cctv-video-live-state-dot {
55
+ width: 4px;
56
+ height: 4px;
57
+ margin-right: 4px;
58
+ border-radius: 4px;
59
+ background: var(--cctv-live-state-dot-active);
60
+ }
61
+
62
+ .cctv-video-live-state:where(.disabled) {
63
+ --badge-fill-bg-color: var(--cctv-live-state-bg-disabled);
64
+ --badge-fill-label-color: var(--cctv-live-state-text-disabled);
65
+ .cctv-video-live-state-dot {
66
+ background: var(--cctv-live-state-dot-disabled);
67
+ }
68
+ }
69
+
70
+ .cctv-video-close-button {
71
+ width: 36px;
72
+ height: 36px;
73
+ border-radius: 50%;
74
+ font-size: 0;
75
+ border: none;
76
+
77
+ display: flex;
78
+ align-items: center;
79
+ justify-content: center;
80
+ cursor: pointer;
81
+ }
82
+
83
+ .cctv-video-overlay-title {
84
+ font-weight: 600;
85
+ color: var(--color-common-99);
86
+ line-height: 1.5em;
87
+ }
88
+ .cctv-video-header-title {
89
+ font-size: var(--font-heading-large-size);
90
+ }
91
+ .cctv-video-footer-title {
92
+ font-size: var(--font-heading-xsmall-size);
93
+ }
94
+
95
+ .cctv-video-overlay-body {
96
+ width: 100%;
97
+ flex: 1;
98
+
99
+ // pointer-events: none;
100
+
101
+ display: flex;
102
+ flex-direction: column;
103
+ align-items: center;
104
+ justify-content: center;
105
+ }
106
+
107
+ .cctv-video-error {
108
+ display: flex;
109
+ flex-direction: column;
110
+ align-items: center;
111
+ justify-content: center;
112
+ }
113
+ .cctv-video-error-icon {
114
+ margin-bottom: 2px;
115
+ fill: var(--cctv-error-icon-color);
116
+ }
117
+ .cctv-video-error-message {
118
+ font-size: 13px;
119
+ color: var(--cctv-error-text-color);
120
+ line-height: 1.5em;
121
+ font-weight: 600;
122
+ }
123
+
124
+ .cctv-video-status-text {
125
+ font-size: 12px;
126
+ color: var(--cctv-text-muted);
127
+ }
128
+
129
+ .cctv-video-overlay-footer {
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: space-between;
133
+ gap: 8px;
134
+ margin-top: auto;
135
+ }
136
+
137
+ .cctv-video-open-button {
138
+ display: flex;
139
+ align-items: center;
140
+ justify-content: center;
141
+ cursor: pointer;
142
+ }
@@ -0,0 +1,7 @@
1
+ .cctv-viewer-desktop-video-container {
2
+ max-height: 500px;
3
+ }
4
+
5
+ .cctv-viewer-desktop-pagination-container {
6
+ margin-top: var(--spacing-gap-8);
7
+ }