@gradio/image 0.14.0 → 0.16.0-beta.1

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.
@@ -0,0 +1,40 @@
1
+ import { SvelteComponent } from "svelte";
2
+ import type { I18nFormatter } from "@gradio/utils";
3
+ import { type FileData, type Client } from "@gradio/client";
4
+ declare const __propDef: {
5
+ props: {
6
+ modify_stream?: ((state: "open" | "closed" | "waiting") => void) | undefined;
7
+ set_time_limit?: ((time: number) => void) | undefined;
8
+ streaming?: boolean | undefined;
9
+ pending?: boolean | undefined;
10
+ root?: string | undefined;
11
+ stream_every?: number | undefined;
12
+ mode?: ("image" | "video") | undefined;
13
+ mirror_webcam: boolean;
14
+ include_audio: boolean;
15
+ i18n: I18nFormatter;
16
+ upload: Client["upload"];
17
+ value?: (FileData | null) | undefined;
18
+ click_outside?: ((node: Node, cb: any) => any) | undefined;
19
+ };
20
+ events: {
21
+ stream: CustomEvent<undefined>;
22
+ capture: CustomEvent<Blob | FileData | null>;
23
+ error: CustomEvent<string>;
24
+ start_recording: CustomEvent<undefined>;
25
+ stop_recording: CustomEvent<undefined>;
26
+ close_stream: CustomEvent<undefined>;
27
+ } & {
28
+ [evt: string]: CustomEvent<any>;
29
+ };
30
+ slots: {};
31
+ };
32
+ export type WebcamProps = typeof __propDef.props;
33
+ export type WebcamEvents = typeof __propDef.events;
34
+ export type WebcamSlots = typeof __propDef.slots;
35
+ export default class Webcam extends SvelteComponent<WebcamProps, WebcamEvents, WebcamSlots> {
36
+ get modify_stream(): (state: "open" | "closed" | "waiting") => void;
37
+ get set_time_limit(): (time: number) => void;
38
+ get click_outside(): (node: Node, cb: any) => any;
39
+ }
40
+ export {};
@@ -0,0 +1,42 @@
1
+ <script>import { Webcam } from "@gradio/icons";
2
+ import { createEventDispatcher } from "svelte";
3
+ const dispatch = createEventDispatcher();
4
+ </script>
5
+
6
+ <button style:height="100%" on:click={() => dispatch("click")}>
7
+ <div class="wrap">
8
+ <span class="icon-wrap">
9
+ <Webcam />
10
+ </span>
11
+ {"Click to Access Webcam"}
12
+ </div>
13
+ </button>
14
+
15
+ <style>
16
+ button {
17
+ cursor: pointer;
18
+ width: var(--size-full);
19
+ }
20
+
21
+ .wrap {
22
+ display: flex;
23
+ flex-direction: column;
24
+ justify-content: center;
25
+ align-items: center;
26
+ min-height: var(--size-60);
27
+ color: var(--block-label-text-color);
28
+ height: 100%;
29
+ padding-top: var(--size-3);
30
+ }
31
+
32
+ .icon-wrap {
33
+ width: 30px;
34
+ margin-bottom: var(--spacing-lg);
35
+ }
36
+
37
+ @media (--screen-md) {
38
+ .wrap {
39
+ font-size: var(--text-lg);
40
+ }
41
+ }
42
+ </style>
@@ -0,0 +1,18 @@
1
+ import { SvelteComponent } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ [x: string]: never;
5
+ };
6
+ events: {
7
+ click: CustomEvent<undefined>;
8
+ } & {
9
+ [evt: string]: CustomEvent<any>;
10
+ };
11
+ slots: {};
12
+ };
13
+ export type WebcamPermissionsProps = typeof __propDef.props;
14
+ export type WebcamPermissionsEvents = typeof __propDef.events;
15
+ export type WebcamPermissionsSlots = typeof __propDef.slots;
16
+ export default class WebcamPermissions extends SvelteComponent<WebcamPermissionsProps, WebcamPermissionsEvents, WebcamPermissionsSlots> {
17
+ }
18
+ export {};
@@ -0,0 +1,2 @@
1
+ export { default as Image } from "./Image.svelte";
2
+ export { default as StaticImage } from "./ImagePreview.svelte";
@@ -0,0 +1,2 @@
1
+ export { default as Image } from "./Image.svelte";
2
+ export { default as StaticImage } from "./ImagePreview.svelte";
@@ -0,0 +1,5 @@
1
+ export declare function get_devices(): Promise<MediaDeviceInfo[]>;
2
+ export declare function handle_error(error: string): void;
3
+ export declare function set_local_stream(local_stream: MediaStream | null, video_source: HTMLVideoElement): void;
4
+ export declare function get_video_stream(include_audio: boolean, video_source: HTMLVideoElement, device_id?: string): Promise<MediaStream>;
5
+ export declare function set_available_devices(devices: MediaDeviceInfo[]): MediaDeviceInfo[];
@@ -0,0 +1,31 @@
1
+ export function get_devices() {
2
+ return navigator.mediaDevices.enumerateDevices();
3
+ }
4
+ export function handle_error(error) {
5
+ throw new Error(error);
6
+ }
7
+ export function set_local_stream(local_stream, video_source) {
8
+ video_source.srcObject = local_stream;
9
+ video_source.muted = true;
10
+ video_source.play();
11
+ }
12
+ export async function get_video_stream(include_audio, video_source, device_id) {
13
+ const size = {
14
+ width: { ideal: 1920 },
15
+ height: { ideal: 1440 }
16
+ };
17
+ const constraints = {
18
+ video: device_id ? { deviceId: { exact: device_id }, ...size } : size,
19
+ audio: include_audio
20
+ };
21
+ return navigator.mediaDevices
22
+ .getUserMedia(constraints)
23
+ .then((local_stream) => {
24
+ set_local_stream(local_stream, video_source);
25
+ return local_stream;
26
+ });
27
+ }
28
+ export function set_available_devices(devices) {
29
+ const cameras = devices.filter((device) => device.kind === "videoinput");
30
+ return cameras;
31
+ }
@@ -0,0 +1 @@
1
+ export declare const get_coordinates_of_clicked_image: (evt: MouseEvent) => [number, number] | null;
@@ -0,0 +1,28 @@
1
+ export const get_coordinates_of_clicked_image = (evt) => {
2
+ let image;
3
+ if (evt.currentTarget instanceof Element) {
4
+ image = evt.currentTarget.querySelector("img");
5
+ }
6
+ else {
7
+ return [NaN, NaN];
8
+ }
9
+ const imageRect = image.getBoundingClientRect();
10
+ const xScale = image.naturalWidth / imageRect.width;
11
+ const yScale = image.naturalHeight / imageRect.height;
12
+ if (xScale > yScale) {
13
+ const displayed_height = image.naturalHeight / xScale;
14
+ const y_offset = (imageRect.height - displayed_height) / 2;
15
+ var x = Math.round((evt.clientX - imageRect.left) * xScale);
16
+ var y = Math.round((evt.clientY - imageRect.top - y_offset) * xScale);
17
+ }
18
+ else {
19
+ const displayed_width = image.naturalWidth / yScale;
20
+ const x_offset = (imageRect.width - displayed_width) / 2;
21
+ var x = Math.round((evt.clientX - imageRect.left - x_offset) * yScale);
22
+ var y = Math.round((evt.clientY - imageRect.top) * yScale);
23
+ }
24
+ if (x < 0 || x >= image.naturalWidth || y < 0 || y >= image.naturalHeight) {
25
+ return null;
26
+ }
27
+ return [x, y];
28
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/image",
3
- "version": "0.14.0",
3
+ "version": "0.16.0-beta.1",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -10,24 +10,48 @@
10
10
  "cropperjs": "^1.5.12",
11
11
  "lazy-brush": "^1.0.1",
12
12
  "resize-observer-polyfill": "^1.5.1",
13
- "@gradio/atoms": "^0.7.9",
14
- "@gradio/icons": "^0.7.0",
15
- "@gradio/statustracker": "^0.7.4",
16
- "@gradio/client": "^1.5.0",
17
- "@gradio/upload": "^0.12.2",
18
- "@gradio/utils": "^0.5.2",
19
- "@gradio/wasm": "^0.12.0"
13
+ "@gradio/atoms": "^0.8.1-beta.1",
14
+ "@gradio/icons": "^0.8.0-beta.1",
15
+ "@gradio/client": "^1.6.0-beta.1",
16
+ "@gradio/utils": "^0.7.0-beta.1",
17
+ "@gradio/wasm": "^0.13.1-beta.1",
18
+ "@gradio/upload": "^0.12.4-beta.1",
19
+ "@gradio/statustracker": "^0.8.0-beta.1"
20
20
  },
21
21
  "devDependencies": {
22
- "@gradio/preview": "^0.10.2"
22
+ "@gradio/preview": "^0.11.1-beta.0"
23
23
  },
24
24
  "main_changeset": true,
25
25
  "main": "./Index.svelte",
26
26
  "exports": {
27
- ".": "./Index.svelte",
28
- "./shared": "./shared/index.ts",
29
- "./example": "./Example.svelte",
30
- "./base": "./shared/ImagePreview.svelte",
31
- "./package.json": "./package.json"
27
+ "./package.json": "./package.json",
28
+ ".": {
29
+ "gradio": "./Index.svelte",
30
+ "svelte": "./dist/Index.svelte",
31
+ "types": "./dist/Index.svelte.d.ts"
32
+ },
33
+ "./example": {
34
+ "gradio": "./Example.svelte",
35
+ "svelte": "./dist/Example.svelte",
36
+ "types": "./dist/Example.svelte.d.ts"
37
+ },
38
+ "./base": {
39
+ "gradio": "./shared/ImagePreview.svelte",
40
+ "svelte": "./dist/shared/ImagePreview.svelte",
41
+ "types": "./dist/shared/ImagePreview.svelte.d.ts"
42
+ },
43
+ "./shared": {
44
+ "gradio": "./shared/index.ts",
45
+ "svelte": "./dist/shared/index.js",
46
+ "types": "./dist/shared/index.d.ts"
47
+ }
48
+ },
49
+ "peerDependencies": {
50
+ "svelte": "^4.0.0"
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/gradio-app/gradio.git",
55
+ "directory": "js/image"
32
56
  }
33
57
  }
@@ -99,7 +99,7 @@
99
99
  {/if}
100
100
  </div>
101
101
  <button on:click={handle_click}>
102
- <div class:selectable>
102
+ <div class:selectable class="image-frame">
103
103
  <Image src={value.url} alt="" loading="lazy" on:load />
104
104
  </div>
105
105
  </button>
@@ -111,12 +111,10 @@
111
111
  height: 100%;
112
112
  position: relative;
113
113
  }
114
- .image-container :global(img),
115
- button {
114
+
115
+ .image-container button {
116
116
  width: var(--size-full);
117
117
  height: var(--size-full);
118
- object-fit: contain;
119
- display: block;
120
118
  border-radius: var(--radius-lg);
121
119
 
122
120
  display: flex;
@@ -124,6 +122,19 @@
124
122
  justify-content: center;
125
123
  }
126
124
 
125
+ .image-frame {
126
+ width: auto;
127
+ height: 100%;
128
+ display: flex;
129
+ align-items: center;
130
+ justify-content: center;
131
+ }
132
+ .image-frame :global(img) {
133
+ width: var(--size-full);
134
+ height: var(--size-full);
135
+ object-fit: scale-down;
136
+ }
137
+
127
138
  .selectable {
128
139
  cursor: crosshair;
129
140
  }
@@ -152,6 +163,6 @@
152
163
  :global(.image-container:fullscreen img) {
153
164
  max-width: 90vw;
154
165
  max-height: 90vh;
155
- object-fit: contain;
166
+ object-fit: scale-down;
156
167
  }
157
168
  </style>
@@ -2,12 +2,16 @@
2
2
  import { createEventDispatcher, tick } from "svelte";
3
3
  import { BlockLabel } from "@gradio/atoms";
4
4
  import { Image as ImageIcon } from "@gradio/icons";
5
- import type { SelectData, I18nFormatter } from "@gradio/utils";
5
+ import {
6
+ type SelectData,
7
+ type I18nFormatter,
8
+ type ValueData
9
+ } from "@gradio/utils";
6
10
  import { get_coordinates_of_clicked_image } from "./utils";
7
11
  import Webcam from "./Webcam.svelte";
8
12
 
9
13
  import { Upload } from "@gradio/upload";
10
- import type { FileData, Client } from "@gradio/client";
14
+ import { FileData, type Client } from "@gradio/client";
11
15
  import ClearImage from "./ClearImage.svelte";
12
16
  import { SelectSource } from "@gradio/atoms";
13
17
  import Image from "./Image.svelte";
@@ -28,14 +32,21 @@
28
32
  export let max_file_size: number | null = null;
29
33
  export let upload: Client["upload"];
30
34
  export let stream_handler: Client["stream"];
35
+ export let stream_every: number;
36
+
37
+ export let modify_stream: (state: "open" | "closed" | "waiting") => void;
38
+ export let set_time_limit: (arg0: number) => void;
31
39
 
32
40
  let upload_input: Upload;
33
41
  let uploading = false;
34
42
  export let active_source: source_type = null;
35
43
 
36
44
  function handle_upload({ detail }: CustomEvent<FileData>): void {
37
- value = detail;
38
- dispatch("upload");
45
+ // only trigger streaming event if streaming
46
+ if (!streaming) {
47
+ value = detail;
48
+ dispatch("upload");
49
+ }
39
50
  }
40
51
 
41
52
  function handle_clear(): void {
@@ -44,17 +55,22 @@
44
55
  dispatch("change", null);
45
56
  }
46
57
 
47
- async function handle_save(img_blob: Blob | any): Promise<void> {
58
+ async function handle_save(
59
+ img_blob: Blob | any,
60
+ event: "change" | "stream" | "upload"
61
+ ): Promise<void> {
48
62
  pending = true;
49
63
  const f = await upload_input.load_files([
50
- new File([img_blob], `webcam.png`)
64
+ new File([img_blob], `image/${streaming ? "jpeg" : "png"}`)
51
65
  ]);
52
66
 
53
- value = f?.[0] || null;
54
-
55
- await tick();
56
-
57
- dispatch(streaming ? "stream" : "change");
67
+ if (event === "change" || event === "upload") {
68
+ value = f?.[0] || null;
69
+ await tick();
70
+ dispatch("change");
71
+ } else {
72
+ dispatch("stream", { value: f?.[0] || null, is_value_data: true });
73
+ }
58
74
  pending = false;
59
75
  }
60
76
 
@@ -63,14 +79,15 @@
63
79
 
64
80
  const dispatch = createEventDispatcher<{
65
81
  change?: never;
66
- stream?: never;
82
+ stream: ValueData;
67
83
  clear?: never;
68
84
  drag: boolean;
69
85
  upload?: never;
70
86
  select: SelectData;
87
+ end_stream: never;
71
88
  }>();
72
89
 
73
- let dragging = false;
90
+ export let dragging = false;
74
91
 
75
92
  $: dispatch("drag", dragging);
76
93
 
@@ -109,7 +126,11 @@
109
126
  }}
110
127
  />
111
128
  {/if}
112
- <div class="upload-container" class:reduced-height={sources.length > 1}>
129
+ <div
130
+ class="upload-container"
131
+ class:reduced-height={sources.length > 1}
132
+ style:width={value ? "auto" : "100%"}
133
+ >
113
134
  <Upload
114
135
  hidden={value !== null || active_source === "webcam"}
115
136
  bind:this={upload_input}
@@ -120,7 +141,7 @@
120
141
  on:error
121
142
  {root}
122
143
  {max_file_size}
123
- disable_click={!sources.includes("upload")}
144
+ disable_click={!sources.includes("upload") || value !== null}
124
145
  {upload}
125
146
  {stream_handler}
126
147
  >
@@ -131,17 +152,22 @@
131
152
  {#if active_source === "webcam" && (streaming || (!streaming && !value))}
132
153
  <Webcam
133
154
  {root}
134
- on:capture={(e) => handle_save(e.detail)}
135
- on:stream={(e) => handle_save(e.detail)}
155
+ {value}
156
+ on:capture={(e) => handle_save(e.detail, "change")}
157
+ on:stream={(e) => handle_save(e.detail, "stream")}
136
158
  on:error
137
159
  on:drag
138
- on:upload={(e) => handle_save(e.detail)}
160
+ on:upload={(e) => handle_save(e.detail, "upload")}
161
+ on:close_stream
139
162
  {mirror_webcam}
163
+ {stream_every}
140
164
  {streaming}
141
165
  mode="image"
142
166
  include_audio={false}
143
167
  {i18n}
144
168
  {upload}
169
+ bind:modify_stream
170
+ bind:set_time_limit
145
171
  />
146
172
  {:else if value !== null && !streaming}
147
173
  <!-- svelte-ignore a11y-click-events-have-key-events-->
@@ -165,7 +191,7 @@
165
191
  .image-frame :global(img) {
166
192
  width: var(--size-full);
167
193
  height: var(--size-full);
168
- object-fit: contain;
194
+ object-fit: scale-down;
169
195
  }
170
196
 
171
197
  .image-frame {
@@ -175,10 +201,13 @@
175
201
  }
176
202
 
177
203
  .upload-container {
204
+ display: flex;
205
+ align-items: center;
206
+ justify-content: center;
207
+
178
208
  height: 100%;
179
209
  flex-shrink: 1;
180
210
  max-height: 100%;
181
- width: 100%;
182
211
  }
183
212
 
184
213
  .reduced-height {
@@ -1,7 +1,14 @@
1
1
  <script lang="ts">
2
2
  import { createEventDispatcher, onMount } from "svelte";
3
- import { Camera, Circle, Square, DropdownArrow } from "@gradio/icons";
3
+ import {
4
+ Camera,
5
+ Circle,
6
+ Square,
7
+ DropdownArrow,
8
+ Spinner
9
+ } from "@gradio/icons";
4
10
  import type { I18nFormatter } from "@gradio/utils";
11
+ import { StreamingBar } from "@gradio/statustracker";
5
12
  import { type FileData, type Client, prepare_files } from "@gradio/client";
6
13
  import WebcamPermissions from "./WebcamPermissions.svelte";
7
14
  import { fade } from "svelte/transition";
@@ -14,17 +21,39 @@
14
21
  let video_source: HTMLVideoElement;
15
22
  let available_video_devices: MediaDeviceInfo[] = [];
16
23
  let selected_device: MediaDeviceInfo | null = null;
24
+ let time_limit: number | null = null;
25
+ let stream_state: "open" | "waiting" | "closed" = "closed";
26
+
27
+ export const modify_stream: (state: "open" | "closed" | "waiting") => void = (
28
+ state: "open" | "closed" | "waiting"
29
+ ) => {
30
+ if (state === "closed") {
31
+ time_limit = null;
32
+ stream_state = "closed";
33
+ value = null;
34
+ } else if (state === "waiting") {
35
+ stream_state = "waiting";
36
+ } else {
37
+ stream_state = "open";
38
+ }
39
+ };
40
+
41
+ export const set_time_limit = (time: number): void => {
42
+ if (recording) time_limit = time;
43
+ };
17
44
 
18
45
  let canvas: HTMLCanvasElement;
19
46
  export let streaming = false;
20
47
  export let pending = false;
21
48
  export let root = "";
49
+ export let stream_every = 1;
22
50
 
23
51
  export let mode: "image" | "video" = "image";
24
52
  export let mirror_webcam: boolean;
25
53
  export let include_audio: boolean;
26
54
  export let i18n: I18nFormatter;
27
55
  export let upload: Client["upload"];
56
+ export let value: FileData | null = null;
28
57
 
29
58
  const dispatch = createEventDispatcher<{
30
59
  stream: undefined;
@@ -32,6 +61,7 @@
32
61
  error: string;
33
62
  start_recording: undefined;
34
63
  stop_recording: undefined;
64
+ close_stream: undefined;
35
65
  }>();
36
66
 
37
67
  onMount(() => (canvas = document.createElement("canvas")));
@@ -108,11 +138,15 @@
108
138
  context.drawImage(video_source, -video_source.videoWidth, 0);
109
139
  }
110
140
 
141
+ if (streaming && (!recording || stream_state === "waiting")) {
142
+ return;
143
+ }
144
+
111
145
  canvas.toBlob(
112
146
  (blob) => {
113
147
  dispatch(streaming ? "stream" : "capture", blob);
114
148
  },
115
- "image/png",
149
+ `image/${streaming ? "jpeg" : "png"}`,
116
150
  0.8
117
151
  );
118
152
  }
@@ -136,10 +170,10 @@
136
170
  "sample." + mimeType.substring(6)
137
171
  );
138
172
  const val = await prepare_files([_video_blob]);
139
- let value = (
173
+ let val_ = (
140
174
  (await upload(val, root))?.filter(Boolean) as FileData[]
141
175
  )[0];
142
- dispatch("capture", value);
176
+ dispatch("capture", val_);
143
177
  dispatch("stop_recording");
144
178
  }
145
179
  };
@@ -181,9 +215,14 @@
181
215
  take_recording();
182
216
  }
183
217
  if (!recording && stream) {
218
+ dispatch("close_stream");
184
219
  stream.getTracks().forEach((track) => track.stop());
185
220
  video_source.srcObject = null;
186
221
  webcam_accessed = false;
222
+ window.setTimeout(() => {
223
+ value = null;
224
+ }, 500);
225
+ value = null;
187
226
  }
188
227
  }
189
228
 
@@ -192,7 +231,7 @@
192
231
  if (video_source && !pending) {
193
232
  take_picture();
194
233
  }
195
- }, 500);
234
+ }, stream_every * 1000);
196
235
  }
197
236
 
198
237
  let options_open = false;
@@ -225,12 +264,18 @@
225
264
  </script>
226
265
 
227
266
  <div class="wrap">
267
+ <StreamingBar {time_limit} />
228
268
  <!-- svelte-ignore a11y-media-has-caption -->
229
269
  <!-- need to suppress for video streaming https://github.com/sveltejs/svelte/issues/5967 -->
230
270
  <video
231
271
  bind:this={video_source}
232
272
  class:flip={mirror_webcam}
233
- class:hide={!webcam_accessed}
273
+ class:hide={!webcam_accessed || (webcam_accessed && !!value)}
274
+ />
275
+ <!-- svelte-ignore a11y-missing-attribute -->
276
+ <img
277
+ src={value?.url}
278
+ class:hide={!webcam_accessed || (webcam_accessed && !value)}
234
279
  />
235
280
  {#if !webcam_accessed}
236
281
  <div
@@ -247,13 +292,26 @@
247
292
  aria-label={mode === "image" ? "capture photo" : "start recording"}
248
293
  >
249
294
  {#if mode === "video" || streaming}
250
- {#if recording}
251
- <div class="icon red" title="stop recording">
252
- <Square />
295
+ {#if streaming && stream_state === "waiting"}
296
+ <div class="icon-with-text" style="width:var(--size-24);">
297
+ <div class="icon color-primary" title="spinner">
298
+ <Spinner />
299
+ </div>
300
+ {i18n("audio.waiting")}
301
+ </div>
302
+ {:else if (streaming && stream_state === "open") || (!streaming && recording)}
303
+ <div class="icon-with-text">
304
+ <div class="icon color-primary" title="stop recording">
305
+ <Square />
306
+ </div>
307
+ {i18n("audio.stop")}
253
308
  </div>
254
309
  {:else}
255
- <div class="icon red" title="start recording">
256
- <Circle />
310
+ <div class="icon-with-text">
311
+ <div class="icon color-primary" title="start recording">
312
+ <Circle />
313
+ </div>
314
+ {i18n("audio.record")}
257
315
  </div>
258
316
  {/if}
259
317
  {:else}
@@ -335,6 +393,14 @@
335
393
  color: var(--button-secondary-text-color);
336
394
  }
337
395
 
396
+ .icon-with-text {
397
+ width: var(--size-20);
398
+ align-items: center;
399
+ margin: 0 var(--spacing-xl);
400
+ display: flex;
401
+ justify-content: space-evenly;
402
+ }
403
+
338
404
  @media (--screen-md) {
339
405
  button {
340
406
  bottom: var(--size-4);
@@ -348,7 +414,6 @@
348
414
  }
349
415
 
350
416
  .icon {
351
- opacity: 0.8;
352
417
  width: 18px;
353
418
  height: 18px;
354
419
  display: flex;
@@ -356,9 +421,9 @@
356
421
  align-items: center;
357
422
  }
358
423
 
359
- .red {
360
- fill: red;
361
- stroke: red;
424
+ .color-primary {
425
+ fill: var(--primary-600);
426
+ stroke: var(--primary-600);
362
427
  }
363
428
 
364
429
  .flip {