@getmicdrop/svelte-components 5.12.0 → 5.13.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/index.spec.js +0 -1
- package/dist/patterns/navigation/Header.svelte +23 -27
- package/dist/patterns/navigation/Header.svelte.d.ts.map +1 -1
- package/dist/primitives/AvatarButton/AvatarButton.svelte +57 -0
- package/dist/primitives/AvatarButton/AvatarButton.svelte.d.ts +18 -0
- package/dist/primitives/AvatarButton/AvatarButton.svelte.d.ts.map +1 -0
- package/dist/primitives/BottomSheet/BottomSheet.spec.js +19 -19
- package/dist/primitives/BottomSheet/BottomSheet.svelte +5 -5
- package/dist/primitives/BottomSheet/BottomSheet.svelte.d.ts +2 -2
- package/dist/primitives/BottomSheet/BottomSheet.svelte.d.ts.map +1 -1
- package/dist/primitives/BottomSheet/BottomSheetWrapper.test.svelte +3 -3
- package/dist/primitives/BottomSheet/BottomSheetWrapper.test.svelte.d.ts +1 -1
- package/dist/primitives/Button/Button.spec.js +8 -8
- package/dist/primitives/Button/Button.svelte +9 -45
- package/dist/primitives/Button/Button.svelte.d.ts.map +1 -1
- package/dist/primitives/CardAction/CardAction.svelte +68 -0
- package/dist/primitives/CardAction/CardAction.svelte.d.ts +20 -0
- package/dist/primitives/CardAction/CardAction.svelte.d.ts.map +1 -0
- package/dist/primitives/Drawer/Drawer.spec.js +33 -33
- package/dist/primitives/Drawer/Drawer.svelte +5 -9
- package/dist/primitives/Drawer/Drawer.svelte.d.ts +2 -3
- package/dist/primitives/Drawer/Drawer.svelte.d.ts.map +1 -1
- package/dist/primitives/LandingButton/LandingButton.svelte +92 -0
- package/dist/primitives/LandingButton/LandingButton.svelte.d.ts +22 -0
- package/dist/primitives/LandingButton/LandingButton.svelte.d.ts.map +1 -0
- package/dist/primitives/MenuItem/MenuItem.svelte +85 -0
- package/dist/primitives/MenuItem/MenuItem.svelte.d.ts +24 -0
- package/dist/primitives/MenuItem/MenuItem.svelte.d.ts.map +1 -0
- package/dist/primitives/Modal/Modal.spec.js +7 -7
- package/dist/primitives/Modal/Modal.stories.svelte +3 -3
- package/dist/primitives/Modal/Modal.svelte +25 -18
- package/dist/primitives/Modal/Modal.svelte.d.ts +5 -5
- package/dist/primitives/Modal/Modal.svelte.d.ts.map +1 -1
- package/dist/primitives/Modal/ModalTestWrapper.svelte +3 -3
- package/dist/primitives/Modal/ModalTestWrapper.svelte.d.ts +2 -2
- package/dist/primitives/NavItem/NavItem.svelte +75 -0
- package/dist/primitives/NavItem/NavItem.svelte.d.ts +20 -0
- package/dist/primitives/NavItem/NavItem.svelte.d.ts.map +1 -0
- package/dist/primitives/SearchResultItem/SearchResultItem.svelte +109 -0
- package/dist/primitives/SearchResultItem/SearchResultItem.svelte.d.ts +26 -0
- package/dist/primitives/SearchResultItem/SearchResultItem.svelte.d.ts.map +1 -0
- package/dist/primitives/SidebarToggle/SidebarToggle.svelte +55 -0
- package/dist/primitives/SidebarToggle/SidebarToggle.svelte.d.ts +18 -0
- package/dist/primitives/SidebarToggle/SidebarToggle.svelte.d.ts.map +1 -0
- package/dist/primitives/index.d.ts +7 -0
- package/dist/primitives/index.js +21 -0
- package/dist/recipes/SuperLogin/SuperLogin.svelte +3 -3
- package/dist/recipes/SuperLogin/SuperLogin.svelte.d.ts.map +1 -1
- package/dist/recipes/inputs/index.d.ts +0 -1
- package/dist/recipes/inputs/index.js +0 -1
- package/dist/recipes/modals/AlertModal.spec.js +2 -2
- package/dist/recipes/modals/AlertModal.svelte +6 -6
- package/dist/recipes/modals/AlertModal.svelte.d.ts +3 -3
- package/dist/recipes/modals/ConfirmationModal.spec.js +2 -2
- package/dist/recipes/modals/ConfirmationModal.svelte +5 -5
- package/dist/recipes/modals/ConfirmationModal.svelte.d.ts +3 -3
- package/dist/recipes/modals/InputModal.spec.js +2 -2
- package/dist/recipes/modals/InputModal.svelte +4 -4
- package/dist/recipes/modals/InputModal.svelte.d.ts +3 -3
- package/dist/recipes/modals/ModalTestWrapper.spec.js +49 -49
- package/dist/recipes/modals/ModalTestWrapper.svelte +3 -3
- package/dist/recipes/modals/ModalTestWrapper.svelte.d.ts +2 -2
- package/dist/recipes/modals/StatusModal.spec.js +2 -2
- package/dist/recipes/modals/StatusModal.svelte +4 -4
- package/dist/recipes/modals/StatusModal.svelte.d.ts +3 -3
- package/dist/stories/ComponentConsolidation.stories.svelte +10 -10
- package/dist/stories/PrimitivesGallery.svelte +25 -21
- package/dist/stories/PrimitivesGallery.svelte.d.ts.map +1 -1
- package/dist/stories/RecipesGallery.spec.js +9 -18
- package/dist/stories/RecipesGallery.svelte +5 -22
- package/dist/stories/RecipesGallery.svelte.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/recipes/inputs/SelectDropdown.spec.d.ts +0 -2
- package/dist/recipes/inputs/SelectDropdown.spec.d.ts.map +0 -1
- package/dist/recipes/inputs/SelectDropdown.spec.js +0 -518
- package/dist/recipes/inputs/SelectDropdown.svelte +0 -171
- package/dist/recipes/inputs/SelectDropdown.svelte.d.ts +0 -16
- package/dist/recipes/inputs/SelectDropdown.svelte.d.ts.map +0 -1
package/dist/index.spec.js
CHANGED
|
@@ -112,7 +112,6 @@ describe('Recipes Layer - Intermediate Index Files', () => {
|
|
|
112
112
|
expect(exports).toContain('PasswordStrengthIndicator');
|
|
113
113
|
expect(exports).toContain('PlaceAutocomplete');
|
|
114
114
|
expect(exports).toContain('Search');
|
|
115
|
-
expect(exports).toContain('SelectDropdown');
|
|
116
115
|
});
|
|
117
116
|
});
|
|
118
117
|
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import Icon from "../../primitives/Icons/Icon.svelte";
|
|
9
9
|
import ChevronLeft from "../../primitives/Icons/ChevronLeft.svelte";
|
|
10
10
|
import Button from "../../primitives/Button/Button.svelte";
|
|
11
|
+
import MenuItem from "../../primitives/MenuItem/MenuItem.svelte";
|
|
12
|
+
import AvatarButton from "../../primitives/AvatarButton/AvatarButton.svelte";
|
|
11
13
|
import { fade, fly } from "svelte/transition";
|
|
12
14
|
import { cubicOut } from "svelte/easing";
|
|
13
15
|
import { portal } from "../../utils/portal.js";
|
|
@@ -89,13 +91,12 @@
|
|
|
89
91
|
<div class="flex items-center gap-3">
|
|
90
92
|
<DarkModeToggle />
|
|
91
93
|
<div class="relative ml-1">
|
|
92
|
-
<
|
|
93
|
-
variant="avatar"
|
|
94
|
+
<AvatarButton
|
|
94
95
|
size="md"
|
|
95
96
|
onclick={() => showDesktopDropdown = !showDesktopDropdown}
|
|
96
97
|
>
|
|
97
98
|
<Avatar src={avatar} rounded size="md" />
|
|
98
|
-
</
|
|
99
|
+
</AvatarButton>
|
|
99
100
|
|
|
100
101
|
{#if showDesktopDropdown}
|
|
101
102
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
@@ -107,22 +108,21 @@
|
|
|
107
108
|
<span class={`block ${typography.xsMuted} mt-0.5`}>{email || ""}</span>
|
|
108
109
|
</div>
|
|
109
110
|
{#each dropdownLinks as { label, href }}
|
|
110
|
-
<
|
|
111
|
-
variant="menu-item"
|
|
111
|
+
<MenuItem
|
|
112
112
|
size="md"
|
|
113
113
|
onclick={() => handleDropdownItemClick(href)}
|
|
114
114
|
>
|
|
115
115
|
{label}
|
|
116
|
-
</
|
|
116
|
+
</MenuItem>
|
|
117
117
|
{/each}
|
|
118
118
|
<div class="h-px bg-gray-200 dark:bg-gray-700"></div>
|
|
119
|
-
<
|
|
120
|
-
|
|
119
|
+
<MenuItem
|
|
120
|
+
danger
|
|
121
121
|
size="md"
|
|
122
122
|
onclick={() => { showDesktopDropdown = false; signoutHandler(); }}
|
|
123
123
|
>
|
|
124
124
|
Sign out
|
|
125
|
-
</
|
|
125
|
+
</MenuItem>
|
|
126
126
|
</div>
|
|
127
127
|
{/if}
|
|
128
128
|
</div>
|
|
@@ -150,23 +150,21 @@
|
|
|
150
150
|
<DarkModeToggle />
|
|
151
151
|
|
|
152
152
|
<div class="relative ml-1">
|
|
153
|
-
<
|
|
154
|
-
variant="avatar"
|
|
153
|
+
<AvatarButton
|
|
155
154
|
size="md"
|
|
156
155
|
class="block md:hidden"
|
|
157
156
|
onclick={() => showMobileSheet = true}
|
|
158
157
|
>
|
|
159
158
|
<Avatar src={avatar} rounded size="md" />
|
|
160
|
-
</
|
|
159
|
+
</AvatarButton>
|
|
161
160
|
|
|
162
|
-
<
|
|
163
|
-
variant="avatar"
|
|
161
|
+
<AvatarButton
|
|
164
162
|
size="md"
|
|
165
163
|
class="hidden md:block"
|
|
166
164
|
onclick={() => showDesktopDropdown = !showDesktopDropdown}
|
|
167
165
|
>
|
|
168
166
|
<Avatar src={avatar} rounded size="md" />
|
|
169
|
-
</
|
|
167
|
+
</AvatarButton>
|
|
170
168
|
|
|
171
169
|
{#if showDesktopDropdown}
|
|
172
170
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
@@ -178,22 +176,21 @@
|
|
|
178
176
|
<span class={`block ${typography.xsMuted} mt-0.5`}>{email || ""}</span>
|
|
179
177
|
</div>
|
|
180
178
|
{#each dropdownLinks as { label, href }}
|
|
181
|
-
<
|
|
182
|
-
variant="menu-item"
|
|
179
|
+
<MenuItem
|
|
183
180
|
size="md"
|
|
184
181
|
onclick={() => handleDropdownItemClick(href)}
|
|
185
182
|
>
|
|
186
183
|
{label}
|
|
187
|
-
</
|
|
184
|
+
</MenuItem>
|
|
188
185
|
{/each}
|
|
189
186
|
<div class="h-px bg-gray-200 dark:bg-gray-700"></div>
|
|
190
|
-
<
|
|
191
|
-
|
|
187
|
+
<MenuItem
|
|
188
|
+
danger
|
|
192
189
|
size="md"
|
|
193
190
|
onclick={() => { showDesktopDropdown = false; signoutHandler(); }}
|
|
194
191
|
>
|
|
195
192
|
Sign out
|
|
196
|
-
</
|
|
193
|
+
</MenuItem>
|
|
197
194
|
</div>
|
|
198
195
|
{/if}
|
|
199
196
|
</div>
|
|
@@ -227,22 +224,21 @@
|
|
|
227
224
|
|
|
228
225
|
<div class="py-2">
|
|
229
226
|
{#each dropdownLinks as { label, href }}
|
|
230
|
-
<
|
|
231
|
-
variant="menu-item"
|
|
227
|
+
<MenuItem
|
|
232
228
|
size="lg"
|
|
233
229
|
onclick={() => handleMobileSheetItemClick(href)}
|
|
234
230
|
>
|
|
235
231
|
{label}
|
|
236
|
-
</
|
|
232
|
+
</MenuItem>
|
|
237
233
|
{/each}
|
|
238
234
|
|
|
239
|
-
<
|
|
240
|
-
|
|
235
|
+
<MenuItem
|
|
236
|
+
danger
|
|
241
237
|
size="lg"
|
|
242
238
|
onclick={() => { showMobileSheet = false; signoutHandler(); }}
|
|
243
239
|
>
|
|
244
240
|
Sign Out
|
|
245
|
-
</
|
|
241
|
+
</MenuItem>
|
|
246
242
|
</div>
|
|
247
243
|
|
|
248
244
|
<div class="border-t-8 border-gray-100 dark:border-gray-700">
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Header.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/patterns/navigation/Header.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Header.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/patterns/navigation/Header.svelte.ts"],"names":[],"mappings":"AAoBE,UAAU,OAAO;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,KAAK;IACb,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,EAAE,CAAC;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAmMH,QAAA,MAAM,MAAM,2CAAwC,CAAC;AACrD,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACxC,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* AvatarButton Component
|
|
4
|
+
* Clickable avatar/image trigger with opacity hover effect.
|
|
5
|
+
*
|
|
6
|
+
* Replaces: Button variant="avatar"
|
|
7
|
+
*/
|
|
8
|
+
import { twMerge } from 'tailwind-merge';
|
|
9
|
+
import type { Snippet } from 'svelte';
|
|
10
|
+
import { buttonAvatarSizes } from '../../tokens/sizing.js';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
/** Size variant */
|
|
14
|
+
size?: 'sm' | 'md' | 'lg';
|
|
15
|
+
/** Disabled state */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Content (typically an Avatar component or image) */
|
|
18
|
+
children?: Snippet;
|
|
19
|
+
/** Additional classes */
|
|
20
|
+
class?: string;
|
|
21
|
+
/** Click handler */
|
|
22
|
+
onclick?: (e: MouseEvent) => void;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
size = 'md',
|
|
28
|
+
disabled = false,
|
|
29
|
+
children,
|
|
30
|
+
class: className = '',
|
|
31
|
+
onclick,
|
|
32
|
+
...restProps
|
|
33
|
+
}: Props = $props();
|
|
34
|
+
|
|
35
|
+
const baseClasses = 'bg-transparent border-transparent rounded-lg focus:outline-hidden transition-all duration-150 ease-out select-none inline-flex items-center justify-center';
|
|
36
|
+
const hoverClasses = 'hover:opacity-80';
|
|
37
|
+
const disabledClasses = 'opacity-50 cursor-not-allowed';
|
|
38
|
+
|
|
39
|
+
let sizeClass = $derived(buttonAvatarSizes[size] || buttonAvatarSizes.md);
|
|
40
|
+
|
|
41
|
+
let classes = $derived(twMerge(
|
|
42
|
+
baseClasses,
|
|
43
|
+
sizeClass,
|
|
44
|
+
disabled ? disabledClasses : `${hoverClasses} cursor-pointer active:scale-[0.97] active:opacity-90`,
|
|
45
|
+
className
|
|
46
|
+
));
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
class={classes}
|
|
52
|
+
{disabled}
|
|
53
|
+
{onclick}
|
|
54
|
+
{...restProps}
|
|
55
|
+
>
|
|
56
|
+
{#if typeof children === 'function'}{@render children()}{:else if children}{children}{/if}
|
|
57
|
+
</button>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Size variant */
|
|
4
|
+
size?: 'sm' | 'md' | 'lg';
|
|
5
|
+
/** Disabled state */
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
/** Content (typically an Avatar component or image) */
|
|
8
|
+
children?: Snippet;
|
|
9
|
+
/** Additional classes */
|
|
10
|
+
class?: string;
|
|
11
|
+
/** Click handler */
|
|
12
|
+
onclick?: (e: MouseEvent) => void;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
declare const AvatarButton: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
type AvatarButton = ReturnType<typeof AvatarButton>;
|
|
17
|
+
export default AvatarButton;
|
|
18
|
+
//# sourceMappingURL=AvatarButton.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AvatarButton.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/primitives/AvatarButton/AvatarButton.svelte.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAIpC,UAAU,KAAK;IACb,mBAAmB;IACnB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yBAAyB;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oBAAoB;IACpB,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IAClC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAqCH,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -14,44 +14,44 @@ describe('BottomSheet Component', () => {
|
|
|
14
14
|
cleanup();
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
it('does not render when
|
|
18
|
-
const { container } = render(BottomSheet, { props: {
|
|
17
|
+
it('does not render when open is false', () => {
|
|
18
|
+
const { container } = render(BottomSheet, { props: { open: false } });
|
|
19
19
|
const backdrop = container.querySelector('.fixed.inset-0');
|
|
20
20
|
expect(backdrop).toBeFalsy();
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
it('renders when
|
|
24
|
-
const { container } = render(BottomSheet, { props: {
|
|
23
|
+
it('renders when open is true', () => {
|
|
24
|
+
const { container } = render(BottomSheet, { props: { open: true } });
|
|
25
25
|
const backdrop = container.querySelector('.fixed.inset-0');
|
|
26
26
|
expect(backdrop).toBeTruthy();
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
it('renders title when provided', () => {
|
|
30
30
|
const { getByText } = render(BottomSheet, {
|
|
31
|
-
props: {
|
|
31
|
+
props: { open: true, title: 'Test Title' },
|
|
32
32
|
});
|
|
33
33
|
expect(getByText('Test Title')).toBeTruthy();
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
it('does not render title when not provided', () => {
|
|
37
|
-
const { container } = render(BottomSheet, { props: {
|
|
37
|
+
const { container } = render(BottomSheet, { props: { open: true } });
|
|
38
38
|
const title = container.querySelector('h3');
|
|
39
39
|
expect(title).toBeFalsy();
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
it('locks body scroll when
|
|
43
|
-
render(BottomSheet, { props: {
|
|
42
|
+
it('locks body scroll when open', async () => {
|
|
43
|
+
render(BottomSheet, { props: { open: true } });
|
|
44
44
|
// Wait for reactive update
|
|
45
45
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
46
46
|
expect(document.body.style.overflow).toBe('hidden');
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
it('unlocks body scroll when
|
|
50
|
-
const { rerender } = render(BottomSheetWrapper, { props: {
|
|
49
|
+
it('unlocks body scroll when closed', async () => {
|
|
50
|
+
const { rerender } = render(BottomSheetWrapper, { props: { open: true } });
|
|
51
51
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
52
52
|
expect(document.body.style.overflow).toBe('hidden');
|
|
53
53
|
|
|
54
|
-
await rerender({
|
|
54
|
+
await rerender({ open: false });
|
|
55
55
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
56
56
|
|
|
57
57
|
expect(document.body.style.overflow).toBe('');
|
|
@@ -64,7 +64,7 @@ describe('BottomSheet Component', () => {
|
|
|
64
64
|
};
|
|
65
65
|
|
|
66
66
|
const { container } = render(BottomSheet, {
|
|
67
|
-
props: {
|
|
67
|
+
props: { open: true, onclose }
|
|
68
68
|
});
|
|
69
69
|
const backdrop = container.querySelector('.fixed.inset-0');
|
|
70
70
|
|
|
@@ -79,7 +79,7 @@ describe('BottomSheet Component', () => {
|
|
|
79
79
|
};
|
|
80
80
|
|
|
81
81
|
const { container } = render(BottomSheet, {
|
|
82
|
-
props: {
|
|
82
|
+
props: { open: true, onclose }
|
|
83
83
|
});
|
|
84
84
|
const sheet = container.querySelector('.bg-white, .dark\\:bg-gray-800');
|
|
85
85
|
|
|
@@ -93,33 +93,33 @@ describe('BottomSheet Component', () => {
|
|
|
93
93
|
closeCalled = true;
|
|
94
94
|
};
|
|
95
95
|
|
|
96
|
-
render(BottomSheet, { props: {
|
|
96
|
+
render(BottomSheet, { props: { open: true, onclose } });
|
|
97
97
|
|
|
98
98
|
await fireEvent.keyDown(window, { key: 'Escape' });
|
|
99
99
|
expect(closeCalled).toBe(true);
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
-
it('does not dispatch close on Escape when not
|
|
102
|
+
it('does not dispatch close on Escape when not open', async () => {
|
|
103
103
|
let closeCalled = false;
|
|
104
104
|
const onclose = () => {
|
|
105
105
|
closeCalled = true;
|
|
106
106
|
};
|
|
107
107
|
|
|
108
|
-
render(BottomSheet, { props: {
|
|
108
|
+
render(BottomSheet, { props: { open: false, onclose } });
|
|
109
109
|
|
|
110
110
|
await fireEvent.keyDown(window, { key: 'Escape' });
|
|
111
111
|
expect(closeCalled).toBe(false);
|
|
112
112
|
});
|
|
113
113
|
|
|
114
114
|
it('has handle bar element', () => {
|
|
115
|
-
const { container } = render(BottomSheet, { props: {
|
|
115
|
+
const { container } = render(BottomSheet, { props: { open: true } });
|
|
116
116
|
// The handle is the gray bar with rounded corners
|
|
117
117
|
const handle = container.querySelector('.w-10.h-1.bg-gray-300');
|
|
118
118
|
expect(handle).toBeTruthy();
|
|
119
119
|
});
|
|
120
120
|
|
|
121
121
|
it('has content area for slot content', () => {
|
|
122
|
-
const { container } = render(BottomSheet, { props: {
|
|
122
|
+
const { container } = render(BottomSheet, { props: { open: true } });
|
|
123
123
|
// The content area has overflow-y-auto class
|
|
124
124
|
const content = container.querySelector('.overflow-y-auto');
|
|
125
125
|
expect(content).toBeTruthy();
|
|
@@ -127,7 +127,7 @@ describe('BottomSheet Component', () => {
|
|
|
127
127
|
|
|
128
128
|
it('cleans up body overflow on destroy', () => {
|
|
129
129
|
document.body.style.overflow = 'hidden';
|
|
130
|
-
const { unmount } = render(BottomSheet, { props: {
|
|
130
|
+
const { unmount } = render(BottomSheet, { props: { open: true } });
|
|
131
131
|
|
|
132
132
|
unmount();
|
|
133
133
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
9
|
/** Whether the bottom sheet is visible */
|
|
10
|
-
|
|
10
|
+
open?: boolean;
|
|
11
11
|
/** Title displayed in the header */
|
|
12
12
|
title?: string;
|
|
13
13
|
/** Callback when the sheet should close */
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
let {
|
|
22
|
-
|
|
22
|
+
open = $bindable(false),
|
|
23
23
|
title = '',
|
|
24
24
|
onclose,
|
|
25
25
|
children,
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function handleKeydown(e: KeyboardEvent) {
|
|
40
|
-
if (e.key === "Escape" &&
|
|
40
|
+
if (e.key === "Escape" && open) {
|
|
41
41
|
close();
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
$effect(() => {
|
|
46
46
|
if (typeof document !== "undefined") {
|
|
47
|
-
if (
|
|
47
|
+
if (open) {
|
|
48
48
|
document.body.style.overflow = "hidden";
|
|
49
49
|
} else {
|
|
50
50
|
document.body.style.overflow = "";
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
|
|
62
62
|
<svelte:window onkeydown={handleKeydown} />
|
|
63
63
|
|
|
64
|
-
{#if
|
|
64
|
+
{#if open}
|
|
65
65
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
66
66
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
67
67
|
<div
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
interface Props {
|
|
3
3
|
/** Whether the bottom sheet is visible */
|
|
4
|
-
|
|
4
|
+
open?: boolean;
|
|
5
5
|
/** Title displayed in the header */
|
|
6
6
|
title?: string;
|
|
7
7
|
/** Callback when the sheet should close */
|
|
@@ -11,7 +11,7 @@ interface Props {
|
|
|
11
11
|
/** Actions slot (footer buttons) */
|
|
12
12
|
actions?: Snippet;
|
|
13
13
|
}
|
|
14
|
-
declare const BottomSheet: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
declare const BottomSheet: import("svelte").Component<Props, {}, "open">;
|
|
15
15
|
type BottomSheet = ReturnType<typeof BottomSheet>;
|
|
16
16
|
export default BottomSheet;
|
|
17
17
|
//# sourceMappingURL=BottomSheet.svelte.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BottomSheet.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/primitives/BottomSheet/BottomSheet.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAIpC,UAAU,KAAK;IACb,0CAA0C;IAC1C,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,oCAAoC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,mBAAmB;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAoFH,QAAA,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"BottomSheet.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/primitives/BottomSheet/BottomSheet.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAIpC,UAAU,KAAK;IACb,0CAA0C;IAC1C,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,oCAAoC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,mBAAmB;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAoFH,QAAA,MAAM,WAAW,+CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
import BottomSheet from './BottomSheet.svelte';
|
|
3
3
|
|
|
4
4
|
interface Props {
|
|
5
|
-
|
|
5
|
+
open?: boolean;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
let {
|
|
8
|
+
let { open = false }: Props = $props();
|
|
9
9
|
</script>
|
|
10
10
|
|
|
11
|
-
<BottomSheet {
|
|
11
|
+
<BottomSheet {open}>
|
|
12
12
|
<div>Test content</div>
|
|
13
13
|
</BottomSheet>
|
|
@@ -175,10 +175,8 @@ describe('Button States', () => {
|
|
|
175
175
|
expect(button.querySelector('svg')).toBeInTheDocument();
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
expect(button).toHaveClass('text-blue-600');
|
|
181
|
-
});
|
|
178
|
+
// Note: nav variant has been extracted to NavItem component
|
|
179
|
+
// This tests that unknown variants fall back to default styling
|
|
182
180
|
|
|
183
181
|
test('Active state for toggle variant', () => {
|
|
184
182
|
const { button } = setupTest({ active: true, variant: 'toggle' });
|
|
@@ -208,16 +206,18 @@ describe('Button as Link', () => {
|
|
|
208
206
|
});
|
|
209
207
|
|
|
210
208
|
describe('Button with Trailing Content', () => {
|
|
211
|
-
test('Button with trailing content
|
|
209
|
+
test('Button with trailing content stays centered (extracted variants use justify-between)', () => {
|
|
212
210
|
const trailing = () => '→';
|
|
213
211
|
const { container } = render(Button, {
|
|
214
212
|
props: {
|
|
215
|
-
variant: '
|
|
216
|
-
children: () => '
|
|
213
|
+
variant: 'default',
|
|
214
|
+
children: () => 'Click Me',
|
|
217
215
|
trailing,
|
|
218
216
|
}
|
|
219
217
|
});
|
|
220
218
|
const button = container.querySelector('button');
|
|
221
|
-
|
|
219
|
+
// Standard Button variants are always centered - left-aligned behavior
|
|
220
|
+
// has been extracted to dedicated components (MenuItem, NavItem, etc.)
|
|
221
|
+
expect(button).toHaveClass('justify-center');
|
|
222
222
|
});
|
|
223
223
|
});
|
|
@@ -35,9 +35,6 @@
|
|
|
35
35
|
import {
|
|
36
36
|
buttonSizes,
|
|
37
37
|
buttonIconSizes,
|
|
38
|
-
buttonAvatarSizes,
|
|
39
|
-
buttonMenuItemSizes,
|
|
40
|
-
buttonCardSizes,
|
|
41
38
|
} from '../../tokens/sizing.js';
|
|
42
39
|
import { triggerHaptic, getHapticForButtonVariant } from '../../utils/haptic.js';
|
|
43
40
|
|
|
@@ -106,6 +103,9 @@
|
|
|
106
103
|
// Size classes imported from centralized tokens
|
|
107
104
|
|
|
108
105
|
// Variant classes with all states in Tailwind (no focus rings - design decision)
|
|
106
|
+
// NOTE: menu-item, avatar, nav, card, search-result, landing, sidebar-toggle, calendar-day, chart-row
|
|
107
|
+
// have been extracted to dedicated components: MenuItem, AvatarButton, NavItem, CardAction,
|
|
108
|
+
// SearchResultItem, LandingButton, SidebarToggle. Use those components instead.
|
|
109
109
|
const variantClasses: Record<string, string> = {
|
|
110
110
|
default: "text-white bg-blue-700 border border-blue-700 hover:bg-blue-800 dark:bg-blue-600 dark:hover:bg-blue-700",
|
|
111
111
|
alternative: "text-gray-900 bg-white border border-gray-200 hover:bg-gray-100 hover:text-blue-700 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700",
|
|
@@ -119,35 +119,13 @@
|
|
|
119
119
|
icon: "text-gray-500 bg-transparent border-transparent hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700",
|
|
120
120
|
toggle: "text-gray-900 bg-gray-100 border border-gray-200 hover:bg-gray-200 dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:hover:bg-gray-600",
|
|
121
121
|
success: "text-white bg-green-600 border border-green-600",
|
|
122
|
-
// Avatar/image trigger - no background, opacity hover
|
|
123
|
-
avatar: "bg-transparent border-transparent hover:opacity-80",
|
|
124
|
-
// Menu items - full width, left-aligned, for dropdowns/sheets and sidebar nav
|
|
125
|
-
"menu-item": "w-full text-left whitespace-nowrap text-gray-900 bg-transparent border-transparent hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600",
|
|
126
|
-
// Danger menu item - red text version
|
|
127
|
-
"menu-item-danger": "w-full text-left whitespace-nowrap text-red-600 bg-transparent border-transparent hover:bg-red-50 dark:text-red-500 dark:hover:bg-gray-600",
|
|
128
|
-
// Selectable card - bordered card-like button for list selections
|
|
129
|
-
card: "w-full text-left text-gray-900 bg-white border border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700",
|
|
130
|
-
// Search result item - blue hover for search dropdowns
|
|
131
|
-
"search-result": "w-full text-left text-gray-900 bg-transparent border-transparent hover:bg-blue-50 focus:bg-blue-50 dark:text-white dark:hover:bg-blue-900/20 dark:focus:bg-blue-900/20",
|
|
132
|
-
// Sidebar toggle - compact pill for sidebar expand/collapse
|
|
133
|
-
"sidebar-toggle": "w-6 h-7 text-gray-900 bg-blue-100 border border-blue-200 hover:bg-blue-200 shadow-lg dark:text-white dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700",
|
|
134
|
-
// Bottom navigation item - vertical layout, transparent, for mobile nav bars
|
|
135
|
-
nav: "flex-col h-full py-2 text-gray-500 bg-transparent border-transparent hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-500",
|
|
136
|
-
// Calendar day cell - base styling with hover, colors overridden via className
|
|
137
|
-
"calendar-day": "border-transparent text-gray-900 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700",
|
|
138
|
-
// Chart row - for leaderboard/chart list items with progress bars
|
|
139
|
-
"chart-row": "w-full text-left text-gray-900 bg-transparent border-transparent hover:bg-gray-50 dark:text-white dark:hover:bg-gray-800",
|
|
140
|
-
// Landing page hero buttons - prominent CTAs with shadow
|
|
141
|
-
landing: "text-white bg-blue-600 border border-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:border-blue-600 dark:hover:bg-blue-700 no-underline hover:no-underline shadow hover:shadow-md",
|
|
142
|
-
"landing-secondary": "text-gray-700 bg-white border border-gray-200 hover:border-gray-400 hover:text-gray-900 dark:text-gray-300 dark:bg-gray-800 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:text-gray-100 no-underline hover:no-underline shadow hover:shadow-md",
|
|
143
122
|
};
|
|
144
123
|
|
|
145
|
-
// Active state classes for ghost
|
|
124
|
+
// Active state classes for ghost and toggle
|
|
125
|
+
// NOTE: menu-item and nav active states are now handled by MenuItem and NavItem components
|
|
146
126
|
const activeClasses: Record<string, string> = {
|
|
147
127
|
ghost: "text-blue-700 bg-blue-50 dark:text-white dark:bg-gray-900",
|
|
148
128
|
toggle: "text-white bg-blue-600 border-blue-600 hover:bg-blue-700 dark:text-white dark:bg-blue-600 dark:border-blue-600 dark:hover:bg-blue-700",
|
|
149
|
-
"menu-item": "bg-blue-50 dark:bg-gray-700",
|
|
150
|
-
nav: "text-blue-600 dark:text-blue-500",
|
|
151
129
|
};
|
|
152
130
|
|
|
153
131
|
// Disabled classes
|
|
@@ -157,14 +135,7 @@
|
|
|
157
135
|
|
|
158
136
|
let sizeClass = $derived((() => {
|
|
159
137
|
if (resolvedVariant === "icon") return buttonIconSizes[size as keyof typeof buttonIconSizes] || buttonIconSizes.sm;
|
|
160
|
-
if (resolvedVariant === "avatar") return buttonAvatarSizes[size as keyof typeof buttonAvatarSizes] || buttonAvatarSizes.md;
|
|
161
|
-
if (resolvedVariant === "menu-item" || resolvedVariant === "menu-item-danger" || resolvedVariant === "search-result") {
|
|
162
|
-
return buttonMenuItemSizes[size as keyof typeof buttonMenuItemSizes] || buttonMenuItemSizes.md;
|
|
163
|
-
}
|
|
164
|
-
if (resolvedVariant === "card") return buttonCardSizes[size as keyof typeof buttonCardSizes] || buttonCardSizes.md;
|
|
165
138
|
if (resolvedVariant === "link") return "text-sm";
|
|
166
|
-
if (resolvedVariant === "sidebar-toggle") return ""; // Fixed dimensions in variant
|
|
167
|
-
if (resolvedVariant === "nav") return ""; // Nav has sizing in variant class
|
|
168
139
|
return buttonSizes[size as keyof typeof buttonSizes] || buttonSizes.md;
|
|
169
140
|
})());
|
|
170
141
|
|
|
@@ -185,21 +156,14 @@ let sizeClass = $derived((() => {
|
|
|
185
156
|
return variantClasses[resolvedVariant] || variantClasses.default;
|
|
186
157
|
})());
|
|
187
158
|
|
|
188
|
-
//
|
|
189
|
-
let isLeftAligned = $derived(
|
|
190
|
-
resolvedVariant === "menu-item" ||
|
|
191
|
-
resolvedVariant === "menu-item-danger" ||
|
|
192
|
-
resolvedVariant === "card" ||
|
|
193
|
-
resolvedVariant === "search-result" ||
|
|
194
|
-
resolvedVariant === "chart-row"
|
|
195
|
-
);
|
|
159
|
+
// Button is always centered - left-aligned variants have been extracted to dedicated components
|
|
160
|
+
let isLeftAligned = $derived(false);
|
|
196
161
|
|
|
197
162
|
// Use justify-between when there's trailing content (e.g., chevrons in nav items)
|
|
198
163
|
let hasTrailing = $derived(typeof trailing === 'function' || trailing);
|
|
199
164
|
|
|
200
|
-
//
|
|
201
|
-
let
|
|
202
|
-
let roundedClass = $derived(isLandingVariant ? "rounded-xl" : "rounded-lg");
|
|
165
|
+
// Standard buttons use rounded-lg - landing variants have been extracted to LandingButton
|
|
166
|
+
let roundedClass = $derived("rounded-lg");
|
|
203
167
|
|
|
204
168
|
// Click handler with optional haptic feedback
|
|
205
169
|
function handleClick(e: MouseEvent) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Button.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/primitives/Button/Button.svelte.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"Button.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/primitives/Button/Button.svelte.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAQpC,UAAU,KAAK;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;IACrC,mFAAmF;IACnF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IAClC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAoLH,QAAA,MAAM,MAAM,2CAAwC,CAAC;AACrD,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACxC,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* CardAction Component
|
|
4
|
+
* Selectable card-like button for list selections.
|
|
5
|
+
*
|
|
6
|
+
* Replaces: Button variant="card"
|
|
7
|
+
*/
|
|
8
|
+
import { twMerge } from 'tailwind-merge';
|
|
9
|
+
import type { Snippet } from 'svelte';
|
|
10
|
+
import { buttonCardSizes } from '../../tokens/sizing.js';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
/** Size variant */
|
|
14
|
+
size?: 'sm' | 'md' | 'lg';
|
|
15
|
+
/** Disabled state */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Selected/active state */
|
|
18
|
+
selected?: boolean;
|
|
19
|
+
/** Content */
|
|
20
|
+
children?: Snippet;
|
|
21
|
+
/** Additional classes */
|
|
22
|
+
class?: string;
|
|
23
|
+
/** Click handler */
|
|
24
|
+
onclick?: (e: MouseEvent) => void;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
size = 'md',
|
|
30
|
+
disabled = false,
|
|
31
|
+
selected = false,
|
|
32
|
+
children,
|
|
33
|
+
class: className = '',
|
|
34
|
+
onclick,
|
|
35
|
+
...restProps
|
|
36
|
+
}: Props = $props();
|
|
37
|
+
|
|
38
|
+
const baseClasses = 'w-full text-left rounded-lg font-medium leading-none focus:outline-hidden transition-all duration-150 ease-out select-none flex items-center justify-start';
|
|
39
|
+
const defaultClasses = 'text-gray-900 bg-white border border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700';
|
|
40
|
+
const selectedClasses = 'text-gray-900 bg-blue-50 border border-blue-500 dark:bg-blue-900/20 dark:text-white dark:border-blue-500';
|
|
41
|
+
const disabledClasses = 'bg-gray-100 border-gray-200 text-gray-400 cursor-not-allowed dark:bg-gray-700 dark:border-gray-700 dark:text-gray-500';
|
|
42
|
+
|
|
43
|
+
let sizeClass = $derived(buttonCardSizes[size] || buttonCardSizes.md);
|
|
44
|
+
|
|
45
|
+
let variantClass = $derived(() => {
|
|
46
|
+
if (disabled) return disabledClasses;
|
|
47
|
+
if (selected) return selectedClasses;
|
|
48
|
+
return defaultClasses;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
let classes = $derived(twMerge(
|
|
52
|
+
baseClasses,
|
|
53
|
+
sizeClass,
|
|
54
|
+
variantClass(),
|
|
55
|
+
disabled ? 'cursor-not-allowed' : 'cursor-pointer active:scale-[0.99] active:opacity-90',
|
|
56
|
+
className
|
|
57
|
+
));
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
class={classes}
|
|
63
|
+
{disabled}
|
|
64
|
+
{onclick}
|
|
65
|
+
{...restProps}
|
|
66
|
+
>
|
|
67
|
+
{#if typeof children === 'function'}{@render children()}{:else if children}{children}{/if}
|
|
68
|
+
</button>
|