@youcan/celeste 0.13.1 → 0.14.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,480 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue';
3
+ import clsx from 'clsx';
4
+ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
5
+
6
+ type Animation = 'blink-twice' | 'blink-once' | 'look-blink-twice' | 'look-blink-once';
7
+ type PeekSide = 'bottom' | 'right' | 'top' | 'left';
8
+ type Peek = '' | 'drop' | 'hidden' | 'out' | 'home';
9
+
10
+ const props = withDefaults(
11
+ defineProps<{
12
+ class?: HTMLAttributes['class'];
13
+ size?: number;
14
+ animate?: boolean;
15
+ thinking?: boolean;
16
+ }>(),
17
+ {
18
+ size: 40,
19
+ animate: false,
20
+ thinking: false,
21
+ },
22
+ );
23
+
24
+ const idle = defineModel<boolean>('idle', { default: false });
25
+
26
+ const DROP_DURATION = 2010;
27
+ const HIDE_DURATION = 420;
28
+ const SETTLE_DURATION = 500;
29
+
30
+ const durations: Record<Animation, number> = { 'blink-twice': 440, 'blink-once': 160, 'look-blink-twice': 3000, 'look-blink-once': 2800 };
31
+ const weights: [Animation, number][] = [['blink-twice', 35], ['blink-once', 35], ['look-blink-twice', 15], ['look-blink-once', 15]];
32
+ const rotation: Record<PeekSide, string> = { bottom: '0deg', right: '-90deg', top: '180deg', left: '90deg' };
33
+ const order: PeekSide[] = ['bottom', 'right', 'top', 'left'];
34
+
35
+ const anim = ref<Animation | ''>('');
36
+ const peek = ref<Peek>('');
37
+ const peekSide = ref<PeekSide>('bottom');
38
+
39
+ const sizePx = computed(() => `${props.size}px`);
40
+ const rotate = computed(() => rotation[peekSide.value]);
41
+
42
+ let retired = false;
43
+ let frame: number | undefined;
44
+ let timer: ReturnType<typeof setTimeout> | undefined;
45
+ let peekTimer: ReturnType<typeof setTimeout> | undefined;
46
+
47
+ function pick(): Animation {
48
+ let r = Math.random() * 100;
49
+ for (const [name, weight] of weights) {
50
+ r -= weight;
51
+ if (r <= 0) {
52
+ return name;
53
+ }
54
+ }
55
+
56
+ return 'blink-twice';
57
+ }
58
+
59
+ function cycle() {
60
+ clearTimeout(timer);
61
+ cancelAnimationFrame(frame ?? 0);
62
+ anim.value = '';
63
+ if (!props.animate || peek.value) {
64
+ return;
65
+ }
66
+ frame = requestAnimationFrame(() => {
67
+ const next = pick();
68
+ anim.value = next;
69
+ timer = setTimeout(cycle, durations[next] + 800 + Math.random() * 1600);
70
+ });
71
+ }
72
+
73
+ function later(fn: () => void, ms: number) {
74
+ clearTimeout(peekTimer);
75
+ peekTimer = setTimeout(fn, ms);
76
+ }
77
+
78
+ function drop() {
79
+ peekSide.value = 'bottom';
80
+ peek.value = 'drop';
81
+ cycle();
82
+ later(() => {
83
+ peek.value = 'out';
84
+ }, DROP_DURATION);
85
+ }
86
+
87
+ function settle() {
88
+ peekSide.value = 'bottom';
89
+ peek.value = 'home';
90
+ later(() => {
91
+ peek.value = '';
92
+ cycle();
93
+ }, SETTLE_DURATION);
94
+ }
95
+
96
+ function hop() {
97
+ peek.value = 'hidden';
98
+ later(() => {
99
+ const next = order[(order.indexOf(peekSide.value) + 1) % order.length];
100
+ if (next === 'bottom') {
101
+ retired = true;
102
+ settle();
103
+ idle.value = false;
104
+
105
+ return;
106
+ }
107
+ peekSide.value = next;
108
+ peek.value = 'out';
109
+ }, HIDE_DURATION);
110
+ }
111
+
112
+ function poke() {
113
+ if (peek.value === 'out') {
114
+ hop();
115
+ }
116
+ else if (!peek.value && props.animate && !retired) {
117
+ idle.value = true;
118
+ }
119
+ }
120
+
121
+ watch(() => idle.value && props.animate, (active) => {
122
+ if (active && !retired && !peek.value) {
123
+ drop();
124
+ }
125
+ else if (!active && peek.value && peek.value !== 'home') {
126
+ peek.value = 'hidden';
127
+ later(settle, HIDE_DURATION);
128
+ }
129
+ }, { immediate: true });
130
+
131
+ watch(() => props.animate, cycle);
132
+ onMounted(cycle);
133
+ onBeforeUnmount(() => {
134
+ cancelAnimationFrame(frame ?? 0);
135
+ clearTimeout(timer);
136
+ clearTimeout(peekTimer);
137
+ });
138
+ </script>
139
+
140
+ <template>
141
+ <div
142
+ :class="clsx(
143
+ 'celeste-mochi',
144
+ peek && `celeste-mochi-peek-${peek}`,
145
+ props.thinking && 'celeste-mochi-thinking',
146
+ props.class,
147
+ )"
148
+ :data-anim="anim || undefined"
149
+ aria-hidden="true"
150
+ >
151
+ <div class="celeste-mochi-stage">
152
+ <div class="celeste-mochi-chan">
153
+ <div class="celeste-mochi-body" @click="poke" />
154
+ <div class="celeste-mochi-eyes">
155
+ <div class="celeste-mochi-eye celeste-mochi-eye-l" />
156
+ <div class="celeste-mochi-eye celeste-mochi-eye-r" />
157
+ </div>
158
+ </div>
159
+ </div>
160
+ </div>
161
+ </template>
162
+
163
+ <style scoped lang="scss">
164
+ .celeste-mochi {
165
+ --mochi-size: v-bind('sizePx');
166
+ --mochi-rotate: v-bind('rotate');
167
+
168
+ position: relative;
169
+ flex: none;
170
+ width: var(--mochi-size);
171
+ height: var(--mochi-size);
172
+ overflow: hidden;
173
+ border-radius: calc(var(--mochi-size) * 0.2222);
174
+ background: linear-gradient(180deg, var(--color-brand-300) 0%, #f90171 100%);
175
+ isolation: isolate;
176
+ }
177
+
178
+ .celeste-mochi::after {
179
+ content: '';
180
+ position: absolute;
181
+ inset: 0;
182
+ border-radius: inherit;
183
+ box-shadow: inset 0 0 0 max(1px, calc(var(--mochi-size) * 0.0278)) var(--color-brand-200);
184
+ pointer-events: none;
185
+ }
186
+
187
+ .celeste-mochi > * {
188
+ position: absolute;
189
+ }
190
+
191
+ .celeste-mochi-stage {
192
+ inset: 0;
193
+ transform: rotate(var(--mochi-rotate));
194
+ }
195
+
196
+ .celeste-mochi-chan {
197
+ position: absolute;
198
+ top: 24%;
199
+ left: -15%;
200
+ width: 129.4%;
201
+ height: 140.9%;
202
+ transition: transform 0.38s cubic-bezier(0.3, -0.4, 0.7, 0.2);
203
+ }
204
+
205
+ .celeste-mochi-body {
206
+ position: absolute;
207
+ inset: 0;
208
+ border-radius: 50%;
209
+ background: var(--color-brand-400);
210
+ box-shadow:
211
+ inset 0 calc(var(--mochi-size) * 0.242) calc(var(--mochi-size) * 0.4015) rgb(255 255 255 / 80%),
212
+ inset 0 calc(var(--mochi-size) * 0.363) calc(var(--mochi-size) * 0.2826) var(--color-brand-300);
213
+ }
214
+
215
+ .celeste-mochi-eyes {
216
+ --eye-top: 12.8%;
217
+ --eye-h: 19.4%;
218
+
219
+ position: absolute;
220
+ transform-origin: 50% calc(var(--eye-top) + var(--eye-h) / 2);
221
+ transition: transform 0.7s ease-in-out;
222
+ pointer-events: none;
223
+ inset: 0;
224
+ }
225
+
226
+ .celeste-mochi-eye {
227
+ position: absolute;
228
+ top: var(--eye-top);
229
+ width: 13.6%;
230
+ height: var(--eye-h);
231
+ overflow: hidden;
232
+ border-radius: var(--radius-full);
233
+ background: linear-gradient(180deg, var(--color-static-white) 0%, rgb(255 255 255 / 60%) 100%);
234
+ filter: drop-shadow(0 calc(var(--mochi-size) * 0.0305) calc(var(--mochi-size) * 0.0153) var(--color-pink-500));
235
+ }
236
+
237
+ .celeste-mochi-eye-l {
238
+ left: 31.5%;
239
+ }
240
+
241
+ .celeste-mochi-eye-r {
242
+ right: 31.5%;
243
+ }
244
+
245
+ .celeste-mochi-peek-drop .celeste-mochi-eye,
246
+ .celeste-mochi-peek-hidden .celeste-mochi-eye,
247
+ .celeste-mochi-peek-out .celeste-mochi-eye {
248
+ height: calc(var(--eye-h) * 80 / 112);
249
+ border-bottom-right-radius: 0;
250
+ border-bottom-left-radius: 0;
251
+ }
252
+
253
+ .celeste-mochi-peek-drop .celeste-mochi-eye {
254
+ animation: celeste-mochi-squint 0.25s ease-in-out 1s both;
255
+ }
256
+
257
+ .celeste-mochi[data-anim='blink-twice'] .celeste-mochi-eye,
258
+ .celeste-mochi[data-anim='look-blink-twice'] .celeste-mochi-eye {
259
+ animation: celeste-mochi-blink-twice 3.5s linear both;
260
+ }
261
+
262
+ .celeste-mochi[data-anim='blink-once'] .celeste-mochi-eye,
263
+ .celeste-mochi[data-anim='look-blink-once'] .celeste-mochi-eye {
264
+ animation: celeste-mochi-blink-once 3.5s linear both;
265
+ }
266
+
267
+ .celeste-mochi[data-anim='look-blink-twice'] .celeste-mochi-eyes {
268
+ animation: celeste-mochi-look-after-blink-twice 3.5s linear both;
269
+ }
270
+
271
+ .celeste-mochi[data-anim='look-blink-once'] .celeste-mochi-eyes {
272
+ animation: celeste-mochi-look-after-blink-once 3.5s linear both;
273
+ }
274
+
275
+ .celeste-mochi-thinking .celeste-mochi-eyes {
276
+ transform: translate(3.77%, -3.118%) rotate(-7deg);
277
+ transition: transform 0.6s ease-out;
278
+ }
279
+
280
+ .celeste-mochi-thinking .celeste-mochi-eye {
281
+ animation: celeste-mochi-blink-twice 3.5s linear 1s infinite;
282
+ }
283
+
284
+ .celeste-mochi-peek-drop .celeste-mochi-chan {
285
+ animation: celeste-mochi-drop 2.01s linear both;
286
+ }
287
+
288
+ .celeste-mochi-peek-hidden .celeste-mochi-chan {
289
+ transform: translateY(calc(var(--mochi-size) * 0.754));
290
+ }
291
+
292
+ .celeste-mochi-peek-out .celeste-mochi-chan {
293
+ transform: translateY(calc(var(--mochi-size) * 0.295));
294
+ transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
295
+ }
296
+
297
+ .celeste-mochi-peek-home .celeste-mochi-chan {
298
+ transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
299
+ }
300
+
301
+ @media (prefers-reduced-motion: reduce) {
302
+ .celeste-mochi * {
303
+ transition-duration: 1ms !important;
304
+ animation-duration: 1ms !important;
305
+ animation-delay: 0s !important;
306
+ animation-iteration-count: 1 !important;
307
+ }
308
+ }
309
+
310
+ @keyframes celeste-mochi-blink-twice {
311
+ 0%,
312
+ 4.57%,
313
+ 8% {
314
+ top: var(--eye-top);
315
+ height: var(--eye-h);
316
+ animation-timing-function: ease-in;
317
+ }
318
+
319
+ 2.29%,
320
+ 10.29% {
321
+ top: calc(var(--eye-top) + var(--eye-h) / 2);
322
+ height: 0;
323
+ animation-timing-function: ease-out;
324
+ }
325
+
326
+ 12.57%,
327
+ 100% {
328
+ top: var(--eye-top);
329
+ height: var(--eye-h);
330
+ }
331
+ }
332
+
333
+ @keyframes celeste-mochi-blink-once {
334
+ 0% {
335
+ top: var(--eye-top);
336
+ height: var(--eye-h);
337
+ animation-timing-function: ease-in;
338
+ }
339
+
340
+ 2.29% {
341
+ top: calc(var(--eye-top) + var(--eye-h) / 2);
342
+ height: 0;
343
+ animation-timing-function: ease-out;
344
+ }
345
+
346
+ 4.57%,
347
+ 100% {
348
+ top: var(--eye-top);
349
+ height: var(--eye-h);
350
+ }
351
+ }
352
+
353
+ @keyframes celeste-mochi-look-after-blink-twice {
354
+ 0%,
355
+ 14.29% {
356
+ transform: translate(0, 0);
357
+ animation-timing-function: ease-out;
358
+ }
359
+
360
+ 22.86%,
361
+ 42.86% {
362
+ transform: translate(3.393%, 1.386%);
363
+ animation-timing-function: ease-in-out;
364
+ }
365
+
366
+ 54.29%,
367
+ 74.29% {
368
+ transform: translate(-3.393%, 1.386%);
369
+ animation-timing-function: ease-in-out;
370
+ }
371
+
372
+ 85.71%,
373
+ 100% {
374
+ transform: translate(0, 0);
375
+ }
376
+ }
377
+
378
+ @keyframes celeste-mochi-look-after-blink-once {
379
+ 0%,
380
+ 8.57% {
381
+ transform: translate(0, 0);
382
+ animation-timing-function: ease-out;
383
+ }
384
+
385
+ 17.14%,
386
+ 37.14% {
387
+ transform: translate(3.393%, 1.386%);
388
+ animation-timing-function: ease-in-out;
389
+ }
390
+
391
+ 48.57%,
392
+ 68.57% {
393
+ transform: translate(-3.393%, 1.386%);
394
+ animation-timing-function: ease-in-out;
395
+ }
396
+
397
+ 80%,
398
+ 100% {
399
+ transform: translate(0, 0);
400
+ }
401
+ }
402
+
403
+ @keyframes celeste-mochi-squint {
404
+ from {
405
+ height: var(--eye-h);
406
+ border-bottom-right-radius: var(--radius-full);
407
+ border-bottom-left-radius: var(--radius-full);
408
+ }
409
+
410
+ to {
411
+ height: calc(var(--eye-h) * 80 / 112);
412
+ border-bottom-right-radius: 0;
413
+ border-bottom-left-radius: 0;
414
+ }
415
+ }
416
+
417
+ @keyframes celeste-mochi-drop {
418
+ 0% {
419
+ transform: translateY(0);
420
+ animation-timing-function: cubic-bezier(0.43, -0.3, 0.74, 0.33);
421
+ }
422
+
423
+ 21.4% {
424
+ transform: translateY(calc(var(--mochi-size) * -0.0082));
425
+ animation-timing-function: cubic-bezier(0.3, -0.05, 0.7, -0.5);
426
+ }
427
+
428
+ 44.28%,
429
+ 68.15% {
430
+ transform: translateY(calc(var(--mochi-size) * 0.754));
431
+ animation-timing-function: linear(
432
+ 0,
433
+ 0.048,
434
+ 0.168,
435
+ 0.326,
436
+ 0.497,
437
+ 0.66,
438
+ 0.804,
439
+ 0.921,
440
+ 1.009,
441
+ 1.068,
442
+ 1.103,
443
+ 1.118,
444
+ 1.118,
445
+ 1.107,
446
+ 1.089,
447
+ 1.069,
448
+ 1.049,
449
+ 1.031,
450
+ 1.015,
451
+ 1.003,
452
+ 0.995,
453
+ 0.989,
454
+ 0.986,
455
+ 0.986,
456
+ 0.987,
457
+ 0.988,
458
+ 0.991,
459
+ 0.993,
460
+ 0.995,
461
+ 0.997,
462
+ 0.999,
463
+ 1,
464
+ 1.001,
465
+ 1.002,
466
+ 1.002,
467
+ 1.002,
468
+ 1.001,
469
+ 1.001,
470
+ 1.001,
471
+ 1.001,
472
+ 1
473
+ );
474
+ }
475
+
476
+ 100% {
477
+ transform: translateY(calc(var(--mochi-size) * 0.295));
478
+ }
479
+ }
480
+ </style>
@@ -0,0 +1,105 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import { ref } from 'vue';
3
+ import Mochi from '../mochi.vue';
4
+
5
+ const meta: Meta<typeof Mochi> = {
6
+ title: 'Components/Mochi',
7
+ component: Mochi,
8
+ argTypes: {
9
+ size: { control: { type: 'number', min: 16, max: 240, step: 4 } },
10
+ },
11
+ };
12
+
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof Mochi>;
16
+
17
+ export const Default: Story = {
18
+ args: {
19
+ size: 80,
20
+ animate: true,
21
+ },
22
+ render: args => ({
23
+ components: { Mochi },
24
+ setup() {
25
+ return { args };
26
+ },
27
+ template: `<Mochi v-bind="args" />`,
28
+ }),
29
+ };
30
+
31
+ export const Static: Story = {
32
+ name: 'Static (Motionless)',
33
+ args: {
34
+ size: 80,
35
+ animate: false,
36
+ },
37
+ render: args => ({
38
+ components: { Mochi },
39
+ setup() {
40
+ return { args };
41
+ },
42
+ template: `<Mochi v-bind="args" />`,
43
+ }),
44
+ };
45
+
46
+ export const Sizes: Story = {
47
+ args: {
48
+ animate: true,
49
+ },
50
+ render: args => ({
51
+ components: { Mochi },
52
+ setup() {
53
+ return { args };
54
+ },
55
+ template: `
56
+ <div style="display: flex; align-items: center; gap: 16px;">
57
+ <Mochi v-bind="args" :size="28" />
58
+ <Mochi v-bind="args" :size="40" />
59
+ <Mochi v-bind="args" :size="80" />
60
+ <Mochi v-bind="args" :size="120" />
61
+ </div>
62
+ `,
63
+ }),
64
+ };
65
+
66
+ export const Thinking: Story = {
67
+ args: {
68
+ size: 80,
69
+ thinking: true,
70
+ },
71
+ render: args => ({
72
+ components: { Mochi },
73
+ setup() {
74
+ return { args };
75
+ },
76
+ template: `<Mochi v-bind="args" />`,
77
+ }),
78
+ };
79
+
80
+ export const Idle: Story = {
81
+ name: 'Idle (Peek-a-boo)',
82
+ args: {
83
+ size: 80,
84
+ animate: true,
85
+ },
86
+ render: args => ({
87
+ components: { Mochi },
88
+ setup() {
89
+ const idle = ref(false);
90
+
91
+ return { args, idle };
92
+ },
93
+ template: `
94
+ <div style="display: flex; flex-direction: column; align-items: flex-start; gap: 12px;">
95
+ <Mochi v-bind="args" v-model:idle="idle" />
96
+ <p style="font: var(--paragraph-sm); color: var(--color-text-sub-600);">
97
+ idle: {{ idle }} — click Mochi to start peeking, then click again to make it hop.
98
+ </p>
99
+ <button type="button" style="font: var(--label-sm);" @click="idle = !idle">
100
+ Toggle idle
101
+ </button>
102
+ </div>
103
+ `,
104
+ }),
105
+ };
@@ -42,7 +42,7 @@ export const Default: Story = {
42
42
  />
43
43
  <ModalBody>
44
44
  <div style="display: grid; gap: 4px;">
45
- <Label label-text="Email address" sublabel="(Optional)" required />
45
+ <Label sublabel="(Optional)" required>Email address</Label>
46
46
  <TextInput type="email" placeholder="hello@live.fr" size="md" />
47
47
  </div>
48
48
  </ModalBody>
@@ -79,7 +79,7 @@ export const WithoutDescription: Story = {
79
79
  />
80
80
  <ModalBody>
81
81
  <div style="display: grid; gap: 4px;">
82
- <Label label-text="Email address" sublabel="(Optional)" required />
82
+ <Label sublabel="(Optional)" required>Email address</Label>
83
83
  <TextInput type="email" placeholder="hello@live.fr" size="md" />
84
84
  </div>
85
85
  </ModalBody>
@@ -113,7 +113,7 @@ export const WithoutIcon: Story = {
113
113
  <ModalHeader title="Email Verification"/>
114
114
  <ModalBody>
115
115
  <div style="display: grid; gap: 4px;">
116
- <Label label-text="Email address" sublabel="(Optional)" required />
116
+ <Label sublabel="(Optional)" required>Email address</Label>
117
117
  <TextInput type="email" placeholder="hello@live.fr" size="md" />
118
118
  </div>
119
119
  </ModalBody>
@@ -38,9 +38,12 @@ const props = withDefaults(defineProps<SidebarMenuActionProps>(), {
38
38
  width: 20px;
39
39
  padding: 0;
40
40
  transition: transform var(--animation-fast) ease-in-out;
41
+ border: 0;
41
42
  border-radius: var(--radius-8);
42
43
  outline: none;
44
+ background: none;
43
45
  color: var(--sidebar-foreground);
46
+ text-decoration: none;
44
47
  inset-inline-end: var(--spacing-4);
45
48
  inset-block-start: var(--spacing-8);
46
49
  aspect-ratio: 1;
@@ -51,10 +51,14 @@ const delegatedProps = reactiveOmit(props, 'class', 'variant', 'isActive');
51
51
  height var(--animation-fast) linear,
52
52
  padding var(--animation-fast) linear,
53
53
  background-color var(--animation-fast);
54
+ border: 0;
54
55
  border-radius: var(--radius-8);
55
56
  outline: none;
57
+ background: none;
58
+ color: var(--sidebar-foreground);
56
59
  font: var(--label-sm);
57
60
  text-align: start;
61
+ text-decoration: none;
58
62
  gap: var(--spacing-8);
59
63
  }
60
64
 
@@ -46,9 +46,12 @@ const props = withDefaults(defineProps<SidebarMenuSubButtonProps>(), {
46
46
  overflow: hidden;
47
47
  transform: translateX(-1px);
48
48
  transition: background-color var(--animation-fast);
49
+ border: 0;
49
50
  border-radius: var(--radius-8);
50
51
  outline: none;
52
+ background: none;
51
53
  color: var(--sidebar-foreground);
54
+ text-decoration: none;
52
55
  gap: var(--spacing-8);
53
56
  }
54
57
 
@@ -21,12 +21,14 @@ const props = defineProps<{
21
21
  display: flex;
22
22
  flex-direction: column;
23
23
  min-width: 0;
24
+ margin-block: 0;
24
25
  margin-inline: var(--spacing-16);
25
26
  padding-block: var(--spacing-4);
26
27
  padding-inline: var(--spacing-10);
27
28
  transform: translateX(1px);
28
29
  gap: var(--spacing-4);
29
30
  border-inline-start: 1px solid var(--sidebar-border);
31
+ list-style: none;
30
32
  }
31
33
 
32
34
  [data-collapsible='icon'] .celeste-sidebar-menu-sub {
@@ -22,6 +22,9 @@ const props = defineProps<{
22
22
  flex-direction: column;
23
23
  width: 100%;
24
24
  min-width: 0;
25
+ margin: 0;
26
+ padding: 0;
25
27
  gap: var(--spacing-4);
28
+ list-style: none;
26
29
  }
27
30
  </style>
@@ -180,7 +180,7 @@ export const Simple: Story = {
180
180
  },
181
181
  template: `
182
182
  <SidebarProvider>
183
- <Sidebar collapsible="icon" side="end" v-bind="args">
183
+ <Sidebar collapsible="icon" v-bind="args">
184
184
  <SidebarHeader>
185
185
  <DropdownMenu>
186
186
  <DropdownMenuTrigger as-child>
@@ -351,7 +351,7 @@ export const Simple: Story = {
351
351
 
352
352
  <div style="padding: 24px;">
353
353
  <SidebarTrigger />
354
- <div>
354
+ </div>
355
355
  </SidebarProvider>
356
356
  `,
357
357
  }),