srcdev-nuxt-components 9.1.43 → 9.1.44

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,208 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
3
+ import DisplayAvatar from "../DisplayAvatar.vue";
4
+ import DisplayChip from "../../../02.molecules/display-chip/DisplayChip.vue";
5
+
6
+ describe("DisplayAvatar", () => {
7
+ // ─── Mount ───────────────────────────────────────────────────────────────
8
+
9
+ it("mounts without error", async () => {
10
+ const wrapper = await mountSuspended(DisplayAvatar);
11
+ expect(wrapper.vm).toBeTruthy();
12
+ });
13
+
14
+ // ─── Snapshots ───────────────────────────────────────────────────────────
15
+
16
+ it("renders correct HTML structure (default)", async () => {
17
+ const wrapper = await mountSuspended(DisplayAvatar, {
18
+ props: { alt: "John Doe" },
19
+ });
20
+ expect(wrapper.html()).toMatchSnapshot();
21
+ });
22
+
23
+ it("renders correct HTML structure (with src)", async () => {
24
+ const wrapper = await mountSuspended(DisplayAvatar, {
25
+ props: { src: "/avatar.jpg", alt: "John Doe", size: "lg" },
26
+ });
27
+ expect(wrapper.html()).toMatchSnapshot();
28
+ });
29
+
30
+ it("renders correct HTML structure (with chip)", async () => {
31
+ const wrapper = await mountSuspended(DisplayAvatar, {
32
+ props: { alt: "John Doe", chip: true },
33
+ });
34
+ expect(wrapper.html()).toMatchSnapshot();
35
+ });
36
+
37
+ // ─── Root element ─────────────────────────────────────────────────────────
38
+
39
+ it("renders as <span> by default", async () => {
40
+ const wrapper = await mountSuspended(DisplayAvatar);
41
+ expect(wrapper.element.tagName).toBe("SPAN");
42
+ });
43
+
44
+ it("renders as the element specified by the as prop", async () => {
45
+ const wrapper = await mountSuspended(DisplayAvatar, {
46
+ props: { as: "div" },
47
+ });
48
+ expect(wrapper.element.tagName).toBe("DIV");
49
+ });
50
+
51
+ it("renders as DisplayChip when chip prop is set", async () => {
52
+ const wrapper = await mountSuspended(DisplayAvatar, {
53
+ props: { chip: true },
54
+ });
55
+ expect(wrapper.findComponent(DisplayChip).exists()).toBe(true);
56
+ });
57
+
58
+ // ─── Base class ───────────────────────────────────────────────────────────
59
+
60
+ it("always has the display-avatar class", async () => {
61
+ const wrapper = await mountSuspended(DisplayAvatar);
62
+ expect(wrapper.classes()).toContain("display-avatar");
63
+ });
64
+
65
+ // ─── Size ─────────────────────────────────────────────────────────────────
66
+
67
+ it("applies md size class by default", async () => {
68
+ const wrapper = await mountSuspended(DisplayAvatar);
69
+ expect(wrapper.classes()).toContain("md");
70
+ });
71
+
72
+ it.each(["xs", "s", "md", "lg", "xl"] as const)("applies %s size class when size='%s'", async (size) => {
73
+ const wrapper = await mountSuspended(DisplayAvatar, { props: { size } });
74
+ expect(wrapper.classes()).toContain(size);
75
+ });
76
+
77
+ // ─── Fallback text ────────────────────────────────────────────────────────
78
+
79
+ it("shows initials derived from alt when no src or text is set", async () => {
80
+ const wrapper = await mountSuspended(DisplayAvatar, {
81
+ props: { alt: "John Doe" },
82
+ });
83
+ expect(wrapper.find("span").text()).toBe("JD");
84
+ });
85
+
86
+ it("caps initials at two characters", async () => {
87
+ const wrapper = await mountSuspended(DisplayAvatar, {
88
+ props: { alt: "Alice Bob Charlie" },
89
+ });
90
+ expect(wrapper.find("span").text()).toBe("AB");
91
+ });
92
+
93
+ it("shows single initial for a single-word alt", async () => {
94
+ const wrapper = await mountSuspended(DisplayAvatar, {
95
+ props: { alt: "Alice" },
96
+ });
97
+ expect(wrapper.find("span").text()).toBe("A");
98
+ });
99
+
100
+ it("shows text prop instead of alt-derived initials when text is set", async () => {
101
+ const wrapper = await mountSuspended(DisplayAvatar, {
102
+ props: { alt: "John Doe", text: "?" },
103
+ });
104
+ expect(wrapper.find("span").text()).toBe("?");
105
+ });
106
+
107
+ it("shows empty fallback when neither src, text, nor alt are set", async () => {
108
+ const wrapper = await mountSuspended(DisplayAvatar);
109
+ expect(wrapper.find("span").text()).toBe("");
110
+ });
111
+
112
+ // ─── Image ────────────────────────────────────────────────────────────────
113
+
114
+ it("renders an image element when src is provided", async () => {
115
+ const wrapper = await mountSuspended(DisplayAvatar, {
116
+ props: { src: "/avatar.jpg" },
117
+ });
118
+ expect(wrapper.find(".avatar-image").exists()).toBe(true);
119
+ });
120
+
121
+ it("does not render an image element when src is not provided", async () => {
122
+ const wrapper = await mountSuspended(DisplayAvatar);
123
+ expect(wrapper.find(".avatar-image").exists()).toBe(false);
124
+ });
125
+
126
+ it("falls back to 'Avatar' as alt text when src is set but alt is not", async () => {
127
+ const wrapper = await mountSuspended(DisplayAvatar, {
128
+ props: { src: "/avatar.jpg" },
129
+ });
130
+ expect(wrapper.find(".avatar-image").attributes("alt")).toBe("Avatar");
131
+ });
132
+
133
+ it("uses the alt prop as alt text on the image", async () => {
134
+ const wrapper = await mountSuspended(DisplayAvatar, {
135
+ props: { src: "/avatar.jpg", alt: "Profile picture" },
136
+ });
137
+ expect(wrapper.find(".avatar-image").attributes("alt")).toBe("Profile picture");
138
+ });
139
+
140
+ // ─── Slots ────────────────────────────────────────────────────────────────
141
+
142
+ it("renders default slot content instead of the fallback", async () => {
143
+ const wrapper = await mountSuspended(DisplayAvatar, {
144
+ props: { alt: "John Doe" },
145
+ slots: { default: "<img class='custom-avatar' src='/x.jpg' alt='x' />" },
146
+ });
147
+ expect(wrapper.find(".custom-avatar").exists()).toBe(true);
148
+ expect(wrapper.text()).not.toContain("JD");
149
+ });
150
+
151
+ it("renders icon slot content", async () => {
152
+ const wrapper = await mountSuspended(DisplayAvatar, {
153
+ slots: { icon: "<span class='status-icon'>●</span>" },
154
+ });
155
+ expect(wrapper.find(".status-icon").exists()).toBe(true);
156
+ });
157
+
158
+ // ─── Chip ─────────────────────────────────────────────────────────────────
159
+
160
+ it("passes chipDefaultConfig when chip=true", async () => {
161
+ const wrapper = await mountSuspended(DisplayAvatar, {
162
+ props: { chip: true },
163
+ });
164
+ const chip = wrapper.findComponent(DisplayChip);
165
+ expect(chip.props("config")).toMatchObject({
166
+ size: "12px",
167
+ maskWidth: "4px",
168
+ offset: "0px",
169
+ angle: "90deg",
170
+ });
171
+ });
172
+
173
+ it("passes custom config when chip is an object", async () => {
174
+ const chipConfig = { size: "16px", maskWidth: "2px", offset: "4px", angle: "45deg" };
175
+ const wrapper = await mountSuspended(DisplayAvatar, {
176
+ props: { chip: chipConfig },
177
+ });
178
+ const chip = wrapper.findComponent(DisplayChip);
179
+ expect(chip.props("config")).toMatchObject(chipConfig);
180
+ });
181
+
182
+ // ─── styleClassPassthrough ────────────────────────────────────────────────
183
+
184
+ it("applies a single styleClassPassthrough string", async () => {
185
+ const wrapper = await mountSuspended(DisplayAvatar, {
186
+ props: { styleClassPassthrough: "online" },
187
+ });
188
+ expect(wrapper.classes()).toContain("online");
189
+ });
190
+
191
+ it("applies multiple styleClassPassthrough classes from an array", async () => {
192
+ const wrapper = await mountSuspended(DisplayAvatar, {
193
+ props: { styleClassPassthrough: ["online", "featured"] },
194
+ });
195
+ expect(wrapper.classes()).toContain("online");
196
+ expect(wrapper.classes()).toContain("featured");
197
+ });
198
+
199
+ it("updates classes when styleClassPassthrough prop changes", async () => {
200
+ const wrapper = await mountSuspended(DisplayAvatar, {
201
+ props: { styleClassPassthrough: ["online"] },
202
+ });
203
+ expect(wrapper.classes()).toContain("online");
204
+ await wrapper.setProps({ styleClassPassthrough: ["offline"] });
205
+ expect(wrapper.classes()).not.toContain("online");
206
+ expect(wrapper.classes()).toContain("offline");
207
+ });
208
+ });
@@ -0,0 +1,11 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`DisplayAvatar > renders correct HTML structure (default) 1`] = `"<span class="display-avatar md" style-class-passthrough=""><span>JD</span></span>"`;
4
+
5
+ exports[`DisplayAvatar > renders correct HTML structure (with chip) 1`] = `
6
+ "<span class="display-chip-core circle display-avatar md" style="--chip-size: 12px; --chip-mask-width: 4px; --chip-offset: 0px; --chip-angle: 90deg;"><span>JD</span>
7
+ <!--v-if-->
8
+ <!--v-if--></span>"
9
+ `;
10
+
11
+ exports[`DisplayAvatar > renders correct HTML structure (with src) 1`] = `"<span class="display-avatar lg" style-class-passthrough=""><img data-nuxt-img="" srcset="/_ipx/_/avatar.jpg 1x, /_ipx/_/avatar.jpg 2x" alt="John Doe" class="avatar-image" src="/_ipx/_/avatar.jpg"></span>"`;
@@ -0,0 +1,189 @@
1
+ <template>
2
+ <component :is="tag" class="display-chip-core" :class="[shape, elementClasses]" :style="chipStyles">
3
+ <slot name="default"></slot>
4
+ <Icon v-if="config?.icon" :name="config.icon" class="chip-icon" />
5
+ <span v-if="config?.label" class="chip-label" :class="`length-${config.label.length}`">{{ validatedLabel }}</span>
6
+ </component>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ import type { DisplayChipConfig } from "~/types/components";
11
+
12
+ interface Props {
13
+ tag?: "div" | "span";
14
+ shape?: "circle" | "square";
15
+ config?: DisplayChipConfig;
16
+ styleClassPassthrough?: string | string[];
17
+ }
18
+
19
+ const props = withDefaults(defineProps<Props>(), {
20
+ tag: "span",
21
+ shape: "circle",
22
+ config: () => ({
23
+ size: "12px",
24
+ maskWidth: "4px",
25
+ offset: "0px",
26
+ angle: "90deg",
27
+ icon: undefined,
28
+ label: undefined,
29
+ }),
30
+ styleClassPassthrough: () => [],
31
+ });
32
+
33
+ const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
34
+
35
+ watch(
36
+ () => props.styleClassPassthrough,
37
+ () => {
38
+ resetElementClasses(props.styleClassPassthrough);
39
+ }
40
+ );
41
+
42
+ const validatedLabel = computed(() => {
43
+ if (!props.config?.label) return props.config?.label;
44
+ if (props.config.label.length > 3) {
45
+ console.warn(
46
+ `DisplayChip: label "${
47
+ props.config.label
48
+ }" exceeds maximum length of 3 characters. Truncating to "${props.config.label.slice(0, 3)}"`
49
+ );
50
+ return props.config.label.slice(0, 3);
51
+ }
52
+ return props.config.label;
53
+ });
54
+
55
+ const chipStyles = computed(() => ({
56
+ "--chip-size": props.config?.size,
57
+ "--chip-mask-width": props.config?.maskWidth,
58
+ "--chip-offset": props.config?.offset,
59
+ "--chip-angle": props.config?.angle,
60
+ }));
61
+ </script>
62
+
63
+ <style lang="css">
64
+ @layer components {
65
+ .display-chip-core {
66
+ --computed-mask-diameter: calc(var(--chip-size) + (var(--chip-mask-width) * 2));
67
+
68
+ &.circle {
69
+ --computed-chip-offset: calc((100% / 2) + var(--chip-offset));
70
+ --computed-position-x: calc(var(--computed-chip-offset) * cos(var(--chip-angle) - 90deg) + (100% / 2));
71
+ --computed-position-y: calc(var(--computed-chip-offset) * sin(var(--chip-angle) - 90deg) + (100% / 2));
72
+ }
73
+
74
+ &.square {
75
+ --circle-x: calc(50% + (50% + var(--chip-offset) + (var(--chip-size) / 2)) * cos(var(--chip-angle) - 90deg));
76
+ --circle-y: calc(50% + (50% + var(--chip-offset) + (var(--chip-size) / 2)) * sin(var(--chip-angle) - 90deg));
77
+ --computed-position-x: clamp(calc(var(--chip-offset) * -1), var(--circle-x), calc(100% + var(--chip-offset)));
78
+ --computed-position-y: clamp(calc(var(--chip-offset) * -1), var(--circle-y), calc(100% + var(--chip-offset)));
79
+ }
80
+
81
+ /* colors */
82
+
83
+ --color-offline: slategrey;
84
+ --color-online: rgb(0, 255, 135);
85
+ --color-idle: rgb(255, 185, 51);
86
+ --color-dnd: rgb(255, 40, 80);
87
+
88
+ position: relative;
89
+ display: inline-block;
90
+
91
+ &::after {
92
+ content: "";
93
+ aspect-ratio: 1;
94
+ background: var(--color-offline);
95
+ position: absolute;
96
+ width: var(--chip-size);
97
+ border-radius: 100%;
98
+ z-index: 1;
99
+ }
100
+ .chip-icon {
101
+ position: absolute;
102
+ width: var(--chip-size);
103
+ height: var(--chip-size);
104
+ border-radius: 100%;
105
+ display: flex;
106
+ align-items: center;
107
+ justify-content: center;
108
+ color: black;
109
+ z-index: 2;
110
+ }
111
+
112
+ .chip-label {
113
+ --_font-size-adjust: 0.7;
114
+ position: absolute;
115
+ width: var(--chip-size);
116
+ height: var(--chip-size);
117
+ border-radius: 100%;
118
+ display: flex;
119
+ align-items: center;
120
+ justify-content: center;
121
+ color: black;
122
+ z-index: 2;
123
+ font-size: calc(var(--chip-size) * var(--_font-size-adjust));
124
+ line-height: 1;
125
+ letter-spacing: -0.05rem;
126
+ user-select: none;
127
+
128
+ &.length-2 {
129
+ --_font-size-adjust: 0.6;
130
+ }
131
+
132
+ &.length-3 {
133
+ --_font-size-adjust: 0.5;
134
+ }
135
+ }
136
+
137
+ & > * {
138
+ /*
139
+ create the cutout mask around the image,
140
+ it's just a radial gradient positioned at the same place as the
141
+ psuedo-element ::after
142
+ */
143
+
144
+ &:not(.chip-icon, .chip-label) {
145
+ mask-image: radial-gradient(
146
+ var(--computed-mask-diameter) var(--computed-mask-diameter) at var(--computed-position-x)
147
+ var(--computed-position-y),
148
+ transparent calc(50% - 0.5px),
149
+ black calc(50% + 0.5px)
150
+ );
151
+ }
152
+ }
153
+
154
+ &.circle {
155
+ &::after,
156
+ .chip-icon,
157
+ .chip-label {
158
+ top: calc(var(--computed-position-y) - (var(--chip-size) / 2));
159
+ left: calc(var(--computed-position-x) - (var(--chip-size) / 2));
160
+ }
161
+ }
162
+
163
+ &.square {
164
+ &::after,
165
+ .chip-icon,
166
+ .chip-label {
167
+ top: calc(var(--computed-position-y) - (var(--chip-size) / 2));
168
+ left: calc(var(--computed-position-x) - (var(--chip-size) / 2));
169
+ }
170
+ }
171
+
172
+ &.online {
173
+ &::after {
174
+ background-color: var(--color-online);
175
+ }
176
+ }
177
+ &.idle {
178
+ &::after {
179
+ background-color: var(--color-idle);
180
+ }
181
+ }
182
+ &.dnd {
183
+ &::after {
184
+ background-color: var(--color-dnd);
185
+ }
186
+ }
187
+ }
188
+ }
189
+ </style>
@@ -1,6 +1,7 @@
1
+ import { computed } from "vue";
1
2
  import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
3
  import StorybookComponent from "../DisplayChip.vue";
3
- import type { DisplayChipConfig } from "../../../types/components";
4
+ import type { DisplayChipConfig } from "~/types/components";
4
5
 
5
6
  // Custom interface for story args
6
7
  interface ChipStoryArgs {
@@ -129,7 +130,7 @@ export default {
129
130
  label: "",
130
131
  status: "offline",
131
132
  useSlot: true,
132
- slotContent: "Avatar content",
133
+ slotContent: "SRC",
133
134
  styleClassPassthrough: [],
134
135
  },
135
136
  } as Meta<typeof StorybookComponent>;
@@ -137,30 +138,43 @@ export default {
137
138
  const Template: StoryFn<ChipStoryArgs> = (args) => ({
138
139
  components: { StorybookComponent },
139
140
  setup() {
140
- const chipConfig: DisplayChipConfig = {
141
- size: `${args.chipSize}px`,
142
- maskWidth: `${args.chipMaskWidth}px`,
143
- offset: `${args.chipOffset}px`,
144
- angle: `${args.chipAngle}deg`,
145
- icon: args.icon || undefined,
146
- label: args.label || undefined,
147
- };
141
+ const chipConfig = computed(
142
+ (): DisplayChipConfig => ({
143
+ size: `${args.chipSize}px`,
144
+ maskWidth: `${args.chipMaskWidth}px`,
145
+ offset: `${args.chipOffset}px`,
146
+ angle: `${args.chipAngle}deg`,
147
+ icon: args.icon || undefined,
148
+ label: args.label || undefined,
149
+ })
150
+ );
148
151
 
149
- const classes = [...(args.styleClassPassthrough || []), args.status];
152
+ const classes = computed(() => [...(args.styleClassPassthrough || []), args.status]);
150
153
 
151
154
  return { args, chipConfig, classes };
152
155
  },
153
156
  template: `
154
- <div style="padding: 40px; display: flex; align-items: center; justify-content: center; min-height: 200px;">
157
+ <div style="display: flex; align-items: center; justify-content: center; height: 100vh;">
155
158
  <StorybookComponent
156
159
  :tag="args.tag"
157
160
  :shape="args.shape"
158
161
  :config="chipConfig"
159
162
  :style-class-passthrough="classes"
160
- style="width: 100px; height: 100px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; color: white; font-weight: 500;"
161
163
  >
162
164
  <template v-if="args.useSlot" #default>
163
- {{ args.slotContent }}
165
+ <div :style="{
166
+ width: '50px',
167
+ height: '50px',
168
+ background: '#64748b',
169
+ borderRadius: args.shape === 'circle' ? '50%' : '4px',
170
+ display: 'flex',
171
+ alignItems: 'center',
172
+ justifyContent: 'center',
173
+ color: '#f8fafc',
174
+ fontWeight: '600',
175
+ fontSize: '1.3rem',
176
+ fontFamily: 'sans-serif',
177
+ }">{{ args.slotContent }}</div>
164
178
  </template>
165
179
  </StorybookComponent>
166
180
  </div>
@@ -179,7 +193,6 @@ export const WithIcon = Template.bind({});
179
193
  WithIcon.args = {
180
194
  status: "online",
181
195
  icon: "mdi:check",
182
- slotContent: "Icon Chip",
183
196
  };
184
197
 
185
198
  // Different Statuses
@@ -257,7 +270,6 @@ SquareShape.args = {
257
270
  shape: "square",
258
271
  status: "online",
259
272
  label: "□",
260
- slotContent: "Square Parent",
261
273
  };
262
274
 
263
275
  // With Offset
@@ -266,7 +278,6 @@ WithOffset.args = {
266
278
  status: "online",
267
279
  chipOffset: 10,
268
280
  label: "10",
269
- slotContent: "Offset Example",
270
281
  };
271
282
 
272
283
  // Multiple Chips Demo
@@ -276,61 +287,34 @@ const MultipleChipsTemplate: StoryFn<ChipStoryArgs> = (args) => ({
276
287
  return { args };
277
288
  },
278
289
  template: `
279
- <div style="padding: 40px; display: flex; gap: 40px; align-items: center; justify-content: center; flex-wrap: wrap;">
290
+ <div style="display: flex; gap: 40px; align-items: center; justify-content: center; height: 100vh; flex-wrap: wrap;">
280
291
  <StorybookComponent
281
- :config="{
282
- size: '12px',
283
- maskWidth: '4px',
284
- offset: '0px',
285
- angle: '45deg',
286
- label: '5'
287
- }"
292
+ :config="{ size: '12px', maskWidth: '4px', offset: '0px', angle: '45deg', label: '5' }"
288
293
  :style-class-passthrough="['online']"
289
- style="width: 80px; height: 80px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white;"
290
294
  >
291
- Online
295
+ <div style="width: 50px; height: 50px; background: #64748b; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #f8fafc; font-weight: 600; font-size: 1.3rem; font-family: sans-serif;">SRC</div>
292
296
  </StorybookComponent>
293
297
 
294
298
  <StorybookComponent
295
- :config="{
296
- size: '10px',
297
- maskWidth: '3px',
298
- offset: '2px',
299
- angle: '315deg',
300
- icon: 'mdi:pause'
301
- }"
299
+ :config="{ size: '10px', maskWidth: '3px', offset: '2px', angle: '315deg', icon: 'mdi:pause' }"
302
300
  :style-class-passthrough="['idle']"
303
- style="width: 80px; height: 80px; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; color: white;"
304
301
  >
305
- Idle
302
+ <div style="width: 50px; height: 50px; background: #64748b; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #f8fafc; font-weight: 600; font-size: 1.3rem; font-family: sans-serif;">SRC</div>
306
303
  </StorybookComponent>
307
304
 
308
305
  <StorybookComponent
309
306
  shape="square"
310
- :config="{
311
- size: '14px',
312
- maskWidth: '2px',
313
- offset: '-5px',
314
- angle: '135deg',
315
- label: 'DND'
316
- }"
307
+ :config="{ size: '14px', maskWidth: '2px', offset: '-5px', angle: '135deg', label: 'DND' }"
317
308
  :style-class-passthrough="['dnd']"
318
- style="width: 80px; height: 80px; background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); border-radius: 8px; display: flex; align-items: center; justify-content: center; color: white;"
319
309
  >
320
- DND
310
+ <div style="width: 50px; height: 50px; background: #64748b; border-radius: 4px; display: flex; align-items: center; justify-content: center; color: #f8fafc; font-weight: 600; font-size: 1.3rem; font-family: sans-serif;">SRC</div>
321
311
  </StorybookComponent>
322
312
 
323
313
  <StorybookComponent
324
- :config="{
325
- size: '16px',
326
- maskWidth: '6px',
327
- offset: '8px',
328
- angle: '90deg'
329
- }"
314
+ :config="{ size: '16px', maskWidth: '6px', offset: '8px', angle: '90deg' }"
330
315
  :style-class-passthrough="['offline']"
331
- style="width: 80px; height: 80px; background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); border-radius: 16px; display: flex; align-items: center; justify-content: center; color: white;"
332
316
  >
333
- Offline
317
+ <div style="width: 50px; height: 50px; background: #64748b; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #f8fafc; font-weight: 600; font-size: 1.3rem; font-family: sans-serif;">SRC</div>
334
318
  </StorybookComponent>
335
319
  </div>
336
320
  `,