yaxa-svelte 1.8.0 → 1.10.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/admin/components/AdminDashboard.svelte +6 -0
- package/dist/admin/components/DevSandbox.svelte +6 -2
- package/dist/admin/components/RecordDrawer.svelte +4 -0
- package/dist/components/elements/Badge.test.d.ts +1 -0
- package/dist/components/elements/Badge.test.js +27 -0
- package/dist/components/elements/Button.svelte +52 -12
- package/dist/components/elements/Button.svelte.d.ts +5 -6
- package/dist/components/elements/Button.test.d.ts +1 -0
- package/dist/components/elements/Button.test.js +39 -0
- package/dist/components/elements/DataTable.dom.test.d.ts +1 -0
- package/dist/components/elements/DataTable.dom.test.js +74 -0
- package/dist/components/elements/DataTable.svelte +11 -12
- package/dist/components/elements/Icon.svelte +32 -1
- package/dist/components/elements/Skeleton.svelte +1 -1
- package/dist/components/elements/Skeleton.svelte.d.ts +3 -3
- package/dist/components/elements/Skeleton.test.d.ts +1 -0
- package/dist/components/elements/Skeleton.test.js +49 -0
- package/dist/components/forms/Checkbox.svelte +3 -0
- package/dist/components/forms/Checkbox.svelte.d.ts +1 -0
- package/dist/components/forms/ColorPicker.svelte +22 -2
- package/dist/components/forms/ColorPicker.svelte.d.ts +5 -0
- package/dist/components/forms/Combobox.dom.test.d.ts +1 -0
- package/dist/components/forms/Combobox.dom.test.js +32 -0
- package/dist/components/forms/Combobox.svelte +3 -0
- package/dist/components/forms/Dropzone.svelte +11 -0
- package/dist/components/forms/Dropzone.svelte.d.ts +4 -0
- package/dist/components/forms/FormField.svelte +6 -1
- package/dist/components/forms/FormField.svelte.d.ts +1 -0
- package/dist/components/forms/InputOTP.svelte +6 -0
- package/dist/components/forms/InputOTP.svelte.d.ts +2 -0
- package/dist/components/forms/InputOTP.test.d.ts +1 -0
- package/dist/components/forms/InputOTP.test.js +61 -0
- package/dist/components/forms/MultiSelect.svelte +3 -0
- package/dist/components/forms/NumberInput.svelte +11 -1
- package/dist/components/forms/NumberInput.svelte.d.ts +4 -0
- package/dist/components/forms/RadioGroup.svelte +2 -0
- package/dist/components/forms/Select.svelte +14 -1
- package/dist/components/forms/Select.svelte.d.ts +5 -0
- package/dist/components/forms/Slider.svelte +14 -1
- package/dist/components/forms/Slider.svelte.d.ts +4 -0
- package/dist/components/forms/Switch.svelte +14 -1
- package/dist/components/forms/Switch.svelte.d.ts +3 -0
- package/dist/components/forms/Switch.test.d.ts +1 -0
- package/dist/components/forms/Switch.test.js +27 -0
- package/dist/components/layout/Footer.svelte +10 -15
- package/dist/components/layout/Header.svelte +5 -4
- package/dist/components/layout/ResizablePanels.svelte +32 -13
- package/dist/components/layout/YaxaApp.svelte +9 -7
- package/dist/components/legal/LegalDocument.svelte +3 -2
- package/dist/components/navigation/CommandPalette.svelte +3 -0
- package/dist/components/navigation/DropdownMenu.svelte +14 -11
- package/dist/components/overlays/Modal.dom.test.d.ts +1 -0
- package/dist/components/overlays/Modal.dom.test.js +31 -0
- package/dist/components/overlays/Modal.test.d.ts +1 -0
- package/dist/components/overlays/Modal.test.js +23 -0
- package/dist/components/overlays/Popover.svelte +5 -1
- package/dist/components/overlays/Tooltip.dom.test.d.ts +1 -0
- package/dist/components/overlays/Tooltip.dom.test.js +14 -0
- package/dist/components/overlays/Tooltip.svelte +29 -23
- package/dist/components/saas/Gate.svelte +3 -2
- package/dist/components/saas/UserMenu.svelte +3 -3
- package/dist/components/seo/Favicons.svelte +37 -12
- package/dist/components/seo/Favicons.svelte.d.ts +4 -0
- package/dist/components/seo/OgImage.svelte +3 -2
- package/dist/components/seo/Seo.svelte +6 -4
- package/dist/composables/useClipboard.test.d.ts +1 -0
- package/dist/composables/useClipboard.test.js +33 -0
- package/dist/composables/useGate.svelte.js +3 -2
- package/dist/composables/useLocale.svelte.js +8 -6
- package/dist/composables/useToast.test.d.ts +1 -0
- package/dist/composables/useToast.test.js +46 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/server/email/email.test.d.ts +1 -0
- package/dist/server/email/email.test.js +25 -0
- package/dist/server/endpoints.test.d.ts +1 -0
- package/dist/server/endpoints.test.js +88 -0
- package/dist/server/storage/storage.test.d.ts +1 -0
- package/dist/server/storage/storage.test.js +62 -0
- package/dist/site/config.d.ts +2 -0
- package/dist/site/context.d.ts +1 -30
- package/dist/site/context.js +1 -43
- package/dist/site/context.svelte.d.ts +37 -0
- package/dist/site/context.svelte.js +52 -0
- package/dist/site/context.test.d.ts +1 -0
- package/dist/site/context.test.js +29 -0
- package/dist/site/og.js +1 -1
- package/dist/styles/yaxa.css +30 -0
- package/dist/testing/index.d.ts +135 -0
- package/dist/testing/index.js +206 -0
- package/dist/testing/index.test.js +87 -1
- package/dist/testing/setup.d.ts +0 -0
- package/dist/testing/setup.js +38 -0
- package/dist/theme/theme.svelte.js +4 -1
- package/dist/vite/index.js +12 -4
- package/dist/vite/index.test.js +6 -6
- package/package.json +19 -13
|
@@ -284,8 +284,11 @@
|
|
|
284
284
|
<div class="flex items-center gap-2">
|
|
285
285
|
<form onsubmit={handleSearch} class="relative">
|
|
286
286
|
<input
|
|
287
|
+
id="admin-search-input"
|
|
288
|
+
name="adminSearch"
|
|
287
289
|
type="search"
|
|
288
290
|
placeholder="Search records..."
|
|
291
|
+
aria-label="Search records"
|
|
289
292
|
bind:value={searchQuery}
|
|
290
293
|
class="w-48 rounded-lg border border-neutral-300 bg-white py-1.5 pr-3 pl-8 text-xs text-neutral-900 placeholder-neutral-400 transition-all focus:w-60 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100"
|
|
291
294
|
/>
|
|
@@ -424,6 +427,7 @@
|
|
|
424
427
|
{#if col.dataType === 'boolean'}
|
|
425
428
|
<input
|
|
426
429
|
id={`create-${col.name}`}
|
|
430
|
+
name={col.name}
|
|
427
431
|
type="checkbox"
|
|
428
432
|
class="h-4 w-4 rounded border-neutral-300 text-primary-600 focus:ring-primary-500"
|
|
429
433
|
checked={Boolean(newRecordData[col.name])}
|
|
@@ -432,6 +436,7 @@
|
|
|
432
436
|
{:else if col.dataType === 'number' || col.dataType === 'integer'}
|
|
433
437
|
<input
|
|
434
438
|
id={`create-${col.name}`}
|
|
439
|
+
name={col.name}
|
|
435
440
|
type="number"
|
|
436
441
|
placeholder={col.primaryKey ? '(Auto-generated if empty)' : ''}
|
|
437
442
|
class="w-full rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-xs dark:border-neutral-700 dark:bg-neutral-900"
|
|
@@ -443,6 +448,7 @@
|
|
|
443
448
|
{:else}
|
|
444
449
|
<input
|
|
445
450
|
id={`create-${col.name}`}
|
|
451
|
+
name={col.name}
|
|
446
452
|
type="text"
|
|
447
453
|
placeholder={col.primaryKey ? '(Auto-generated UUID if empty)' : ''}
|
|
448
454
|
class="w-full rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-xs dark:border-neutral-700 dark:bg-neutral-900"
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import Icon from '../../components/elements/Icon.svelte';
|
|
6
6
|
import Progress from '../../components/elements/Progress.svelte';
|
|
7
7
|
import { useUpload } from '../../composables/useUpload.svelte';
|
|
8
|
-
import {
|
|
8
|
+
import { useYaxa } from '../../site/context';
|
|
9
9
|
|
|
10
10
|
interface Props {
|
|
11
11
|
apiPrefix?: string;
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
let { apiPrefix = '/admin/api', class: className = '' }: Props = $props();
|
|
16
16
|
|
|
17
17
|
// Auth & Context info
|
|
18
|
-
const
|
|
18
|
+
const yaxa = useYaxa();
|
|
19
|
+
const user = $derived(yaxa.user);
|
|
19
20
|
|
|
20
21
|
// Email Sandbox State
|
|
21
22
|
let emailTo = $state('developer@example.com');
|
|
@@ -96,6 +97,7 @@
|
|
|
96
97
|
>
|
|
97
98
|
<input
|
|
98
99
|
id="email-to"
|
|
100
|
+
name="emailTo"
|
|
99
101
|
type="email"
|
|
100
102
|
bind:value={emailTo}
|
|
101
103
|
placeholder="you@example.com"
|
|
@@ -109,6 +111,7 @@
|
|
|
109
111
|
>
|
|
110
112
|
<select
|
|
111
113
|
id="email-template"
|
|
114
|
+
name="emailTemplate"
|
|
112
115
|
bind:value={emailTemplate}
|
|
113
116
|
class="w-full rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-xs text-neutral-900 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100"
|
|
114
117
|
>
|
|
@@ -172,6 +175,7 @@
|
|
|
172
175
|
>
|
|
173
176
|
<input
|
|
174
177
|
id="storage-file"
|
|
178
|
+
name="storageFile"
|
|
175
179
|
type="file"
|
|
176
180
|
onchange={(e) => {
|
|
177
181
|
const files = e.currentTarget.files;
|
|
@@ -138,6 +138,7 @@
|
|
|
138
138
|
<div class="flex items-center gap-2 pt-1">
|
|
139
139
|
<input
|
|
140
140
|
id={`field-${col.name}`}
|
|
141
|
+
name={col.name}
|
|
141
142
|
type="checkbox"
|
|
142
143
|
class="h-4 w-4 rounded border-neutral-300 text-primary-600 focus:ring-primary-500 dark:border-neutral-700 dark:bg-neutral-800"
|
|
143
144
|
checked={Boolean(formData[col.name])}
|
|
@@ -150,6 +151,7 @@
|
|
|
150
151
|
{:else if col.dataType === 'number' || col.dataType === 'integer'}
|
|
151
152
|
<input
|
|
152
153
|
id={`field-${col.name}`}
|
|
154
|
+
name={col.name}
|
|
153
155
|
type="number"
|
|
154
156
|
class="w-full rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-sm text-neutral-900 placeholder-neutral-400 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 focus:outline-none disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100"
|
|
155
157
|
disabled={col.primaryKey}
|
|
@@ -162,6 +164,7 @@
|
|
|
162
164
|
{:else if String(formData[col.name] ?? '').length > 60 || col.name.includes('description') || col.name.includes('body')}
|
|
163
165
|
<textarea
|
|
164
166
|
id={`field-${col.name}`}
|
|
167
|
+
name={col.name}
|
|
165
168
|
rows={3}
|
|
166
169
|
class="w-full rounded-md border border-neutral-300 bg-white px-3 py-1.5 font-mono text-sm text-xs text-neutral-900 placeholder-neutral-400 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 focus:outline-none disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100"
|
|
167
170
|
disabled={col.primaryKey}
|
|
@@ -172,6 +175,7 @@
|
|
|
172
175
|
{:else}
|
|
173
176
|
<input
|
|
174
177
|
id={`field-${col.name}`}
|
|
178
|
+
name={col.name}
|
|
175
179
|
type="text"
|
|
176
180
|
class="w-full rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-sm text-neutral-900 placeholder-neutral-400 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 focus:outline-none disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100"
|
|
177
181
|
disabled={col.primaryKey}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { badgeVariants } from './Badge.svelte';
|
|
3
|
+
describe('Badge Component', () => {
|
|
4
|
+
it('generates solid primary badge classes by default', () => {
|
|
5
|
+
const classes = badgeVariants({ variant: 'solid', color: 'primary' });
|
|
6
|
+
expect(classes).toContain('bg-primary-600');
|
|
7
|
+
expect(classes).toContain('inline-flex');
|
|
8
|
+
expect(classes).toContain('font-medium');
|
|
9
|
+
});
|
|
10
|
+
it('supports multiple variants: outline, soft, subtle', () => {
|
|
11
|
+
const outline = badgeVariants({ variant: 'outline', color: 'success' });
|
|
12
|
+
expect(outline).toContain('border');
|
|
13
|
+
expect(outline).toContain('bg-transparent');
|
|
14
|
+
const soft = badgeVariants({ variant: 'soft', color: 'warning' });
|
|
15
|
+
expect(soft).toContain('inline-flex');
|
|
16
|
+
const subtle = badgeVariants({ variant: 'subtle', color: 'error' });
|
|
17
|
+
expect(subtle).toContain('border');
|
|
18
|
+
});
|
|
19
|
+
it('supports size scales (xs, sm, md, lg)', () => {
|
|
20
|
+
const xs = badgeVariants({ size: 'xs' });
|
|
21
|
+
expect(xs).toContain('text-[10px]');
|
|
22
|
+
expect(xs).toContain('px-1.5');
|
|
23
|
+
const lg = badgeVariants({ size: 'lg' });
|
|
24
|
+
expect(lg).toContain('text-sm');
|
|
25
|
+
expect(lg).toContain('px-3');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -193,48 +193,48 @@
|
|
|
193
193
|
});
|
|
194
194
|
|
|
195
195
|
export type ButtonProps = VariantProps<typeof buttonVariants> & {
|
|
196
|
+
type?: 'button' | 'submit' | 'reset';
|
|
196
197
|
href?: string;
|
|
197
|
-
|
|
198
|
+
as?: 'button' | 'a' | 'span' | 'div';
|
|
198
199
|
disabled?: boolean;
|
|
200
|
+
loading?: boolean;
|
|
199
201
|
icon?: IconSource;
|
|
200
202
|
trailingIcon?: IconSource;
|
|
201
|
-
type?: 'button' | 'submit' | 'reset';
|
|
202
|
-
class?: string;
|
|
203
203
|
leading?: Snippet;
|
|
204
204
|
trailing?: Snippet;
|
|
205
205
|
children?: Snippet;
|
|
206
|
-
onclick?: (
|
|
207
|
-
|
|
208
|
-
'aria-current'?: 'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false' | boolean;
|
|
206
|
+
onclick?: (event: MouseEvent) => void;
|
|
207
|
+
class?: string;
|
|
209
208
|
[key: string]: unknown;
|
|
210
209
|
};
|
|
211
210
|
</script>
|
|
212
211
|
|
|
213
212
|
<script lang="ts">
|
|
214
|
-
import Icon from './Icon.svelte';
|
|
215
213
|
import Spinner from './Spinner.svelte';
|
|
214
|
+
import Icon from './Icon.svelte';
|
|
216
215
|
|
|
217
216
|
let {
|
|
217
|
+
type = 'button',
|
|
218
218
|
href,
|
|
219
|
+
as = 'button',
|
|
219
220
|
variant = 'solid',
|
|
220
221
|
color = 'primary',
|
|
221
222
|
size = 'md',
|
|
222
223
|
block = false,
|
|
223
224
|
square = false,
|
|
224
|
-
loading = false,
|
|
225
225
|
disabled = false,
|
|
226
|
+
loading = false,
|
|
226
227
|
icon,
|
|
227
228
|
trailingIcon,
|
|
228
|
-
type = 'button',
|
|
229
|
-
class: className = '',
|
|
230
229
|
leading,
|
|
231
230
|
trailing,
|
|
232
231
|
children,
|
|
233
232
|
onclick,
|
|
233
|
+
class: className = '',
|
|
234
234
|
...restProps
|
|
235
235
|
}: ButtonProps = $props();
|
|
236
236
|
|
|
237
|
-
|
|
237
|
+
const classes = $derived(
|
|
238
238
|
buttonVariants({
|
|
239
239
|
variant,
|
|
240
240
|
color,
|
|
@@ -246,7 +246,47 @@
|
|
|
246
246
|
);
|
|
247
247
|
</script>
|
|
248
248
|
|
|
249
|
-
{#if
|
|
249
|
+
{#if as === 'span'}
|
|
250
|
+
<span class={classes} aria-disabled={disabled || loading} {onclick} {...restProps}>
|
|
251
|
+
{#if loading}
|
|
252
|
+
<Spinner size={size === 'xs' || size === 'sm' ? 'sm' : 'md'} />
|
|
253
|
+
{:else if leading}
|
|
254
|
+
{@render leading()}
|
|
255
|
+
{:else if icon}
|
|
256
|
+
<Icon name={icon} size={size === 'xs' ? 'xs' : size === 'sm' ? 'sm' : 'md'} />
|
|
257
|
+
{/if}
|
|
258
|
+
|
|
259
|
+
{#if children}
|
|
260
|
+
{@render children()}
|
|
261
|
+
{/if}
|
|
262
|
+
|
|
263
|
+
{#if trailing}
|
|
264
|
+
{@render trailing()}
|
|
265
|
+
{:else if trailingIcon}
|
|
266
|
+
<Icon name={trailingIcon} size={size === 'xs' ? 'xs' : size === 'sm' ? 'sm' : 'md'} />
|
|
267
|
+
{/if}
|
|
268
|
+
</span>
|
|
269
|
+
{:else if as === 'div'}
|
|
270
|
+
<div class={classes} aria-disabled={disabled || loading} {onclick} {...restProps}>
|
|
271
|
+
{#if loading}
|
|
272
|
+
<Spinner size={size === 'xs' || size === 'sm' ? 'sm' : 'md'} />
|
|
273
|
+
{:else if leading}
|
|
274
|
+
{@render leading()}
|
|
275
|
+
{:else if icon}
|
|
276
|
+
<Icon name={icon} size={size === 'xs' ? 'xs' : size === 'sm' ? 'sm' : 'md'} />
|
|
277
|
+
{/if}
|
|
278
|
+
|
|
279
|
+
{#if children}
|
|
280
|
+
{@render children()}
|
|
281
|
+
{/if}
|
|
282
|
+
|
|
283
|
+
{#if trailing}
|
|
284
|
+
{@render trailing()}
|
|
285
|
+
{:else if trailingIcon}
|
|
286
|
+
<Icon name={trailingIcon} size={size === 'xs' ? 'xs' : size === 'sm' ? 'sm' : 'md'} />
|
|
287
|
+
{/if}
|
|
288
|
+
</div>
|
|
289
|
+
{:else if href || as === 'a'}
|
|
250
290
|
<a
|
|
251
291
|
{href}
|
|
252
292
|
class={classes}
|
|
@@ -90,19 +90,18 @@ export declare const buttonVariants: import("tailwind-variants").TVReturnType<{
|
|
|
90
90
|
};
|
|
91
91
|
}, undefined>>;
|
|
92
92
|
export type ButtonProps = VariantProps<typeof buttonVariants> & {
|
|
93
|
+
type?: 'button' | 'submit' | 'reset';
|
|
93
94
|
href?: string;
|
|
94
|
-
|
|
95
|
+
as?: 'button' | 'a' | 'span' | 'div';
|
|
95
96
|
disabled?: boolean;
|
|
97
|
+
loading?: boolean;
|
|
96
98
|
icon?: IconSource;
|
|
97
99
|
trailingIcon?: IconSource;
|
|
98
|
-
type?: 'button' | 'submit' | 'reset';
|
|
99
|
-
class?: string;
|
|
100
100
|
leading?: Snippet;
|
|
101
101
|
trailing?: Snippet;
|
|
102
102
|
children?: Snippet;
|
|
103
|
-
onclick?: (
|
|
104
|
-
|
|
105
|
-
'aria-current'?: 'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false' | boolean;
|
|
103
|
+
onclick?: (event: MouseEvent) => void;
|
|
104
|
+
class?: string;
|
|
106
105
|
[key: string]: unknown;
|
|
107
106
|
};
|
|
108
107
|
declare const Button: import("svelte").Component<ButtonProps, {}, "">;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buttonVariants } from './Button.svelte';
|
|
3
|
+
describe('Button Component', () => {
|
|
4
|
+
it('generates solid primary button classes by default', () => {
|
|
5
|
+
const classes = buttonVariants({ variant: 'solid', color: 'primary' });
|
|
6
|
+
expect(classes).toContain('bg-primary-600');
|
|
7
|
+
expect(classes).toContain('inline-flex');
|
|
8
|
+
expect(classes).toContain('select-none');
|
|
9
|
+
});
|
|
10
|
+
it('supports multiple variants: outline, ghost, soft, subtle, link', () => {
|
|
11
|
+
const outline = buttonVariants({ variant: 'outline', color: 'neutral' });
|
|
12
|
+
expect(outline).toContain('border');
|
|
13
|
+
expect(outline).toContain('bg-transparent');
|
|
14
|
+
const ghost = buttonVariants({ variant: 'ghost', color: 'error' });
|
|
15
|
+
expect(ghost).toContain('bg-transparent');
|
|
16
|
+
const link = buttonVariants({ variant: 'link' });
|
|
17
|
+
expect(link).toContain('underline-offset-4');
|
|
18
|
+
});
|
|
19
|
+
it('supports size scales and square/block modifiers', () => {
|
|
20
|
+
const xs = buttonVariants({ size: 'xs' });
|
|
21
|
+
expect(xs).toContain('text-xs');
|
|
22
|
+
expect(xs).toContain('px-2');
|
|
23
|
+
const xl = buttonVariants({ size: 'xl' });
|
|
24
|
+
expect(xl).toContain('text-base');
|
|
25
|
+
expect(xl).toContain('px-5');
|
|
26
|
+
const block = buttonVariants({ block: true });
|
|
27
|
+
expect(block).toContain('w-full');
|
|
28
|
+
const square = buttonVariants({ square: true });
|
|
29
|
+
expect(square).toContain('aspect-square');
|
|
30
|
+
});
|
|
31
|
+
it('applies compound color classes for success, warning, and error', () => {
|
|
32
|
+
const success = buttonVariants({ variant: 'solid', color: 'success' });
|
|
33
|
+
expect(success).toContain('bg-emerald-600');
|
|
34
|
+
const warning = buttonVariants({ variant: 'solid', color: 'warning' });
|
|
35
|
+
expect(warning).toContain('bg-amber-600');
|
|
36
|
+
const error = buttonVariants({ variant: 'solid', color: 'error' });
|
|
37
|
+
expect(error).toContain('bg-rose-600');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { flushSync } from 'svelte';
|
|
3
|
+
import { renderComponent } from '../../testing';
|
|
4
|
+
import DataTable, {} from './DataTable.svelte';
|
|
5
|
+
const testData = [
|
|
6
|
+
{ id: '1', name: 'Alice Smith', role: 'Engineer', score: 95 },
|
|
7
|
+
{ id: '2', name: 'Bob Jones', role: 'Designer', score: 82 },
|
|
8
|
+
{ id: '3', name: 'Charlie Brown', role: 'Manager', score: 88 }
|
|
9
|
+
];
|
|
10
|
+
const testColumns = [
|
|
11
|
+
{ key: 'name', label: 'Full Name', sortable: true },
|
|
12
|
+
{ key: 'role', label: 'Job Role', sortable: true },
|
|
13
|
+
{ key: 'score', label: 'Performance Score', sortable: true }
|
|
14
|
+
];
|
|
15
|
+
describe('DataTable Component (DOM & Interactions)', () => {
|
|
16
|
+
it('renders table headers and rows accurately', () => {
|
|
17
|
+
const { target, cleanup } = renderComponent(DataTable, {
|
|
18
|
+
data: testData,
|
|
19
|
+
columns: testColumns
|
|
20
|
+
});
|
|
21
|
+
const thElements = target.querySelectorAll('thead th');
|
|
22
|
+
expect(thElements.length).toBe(3);
|
|
23
|
+
expect(thElements[0].textContent).toContain('Full Name');
|
|
24
|
+
expect(thElements[1].textContent).toContain('Job Role');
|
|
25
|
+
const trElements = target.querySelectorAll('tbody tr');
|
|
26
|
+
expect(trElements.length).toBe(3);
|
|
27
|
+
expect(trElements[0].textContent).toContain('Alice Smith');
|
|
28
|
+
expect(trElements[1].textContent).toContain('Bob Jones');
|
|
29
|
+
cleanup();
|
|
30
|
+
});
|
|
31
|
+
it('filters rows according to searchQuery', () => {
|
|
32
|
+
const { target, cleanup } = renderComponent(DataTable, {
|
|
33
|
+
data: testData,
|
|
34
|
+
columns: testColumns,
|
|
35
|
+
searchQuery: 'Designer'
|
|
36
|
+
});
|
|
37
|
+
const trElements = target.querySelectorAll('tbody tr');
|
|
38
|
+
expect(trElements.length).toBe(1);
|
|
39
|
+
expect(trElements[0].textContent).toContain('Bob Jones');
|
|
40
|
+
expect(trElements[0].textContent).toContain('Designer');
|
|
41
|
+
cleanup();
|
|
42
|
+
});
|
|
43
|
+
it('displays custom emptyText when no matching rows are found', () => {
|
|
44
|
+
const { target, cleanup } = renderComponent(DataTable, {
|
|
45
|
+
data: testData,
|
|
46
|
+
columns: testColumns,
|
|
47
|
+
searchQuery: 'NonExistentQueryXYZ',
|
|
48
|
+
emptyText: 'No members found'
|
|
49
|
+
});
|
|
50
|
+
const trElements = target.querySelectorAll('tbody tr');
|
|
51
|
+
expect(trElements.length).toBe(1);
|
|
52
|
+
expect(trElements[0].textContent).toContain('No members found');
|
|
53
|
+
cleanup();
|
|
54
|
+
});
|
|
55
|
+
it('sorts rows when clicking on sortable column headers', () => {
|
|
56
|
+
const { target, cleanup } = renderComponent(DataTable, {
|
|
57
|
+
data: testData,
|
|
58
|
+
columns: testColumns
|
|
59
|
+
});
|
|
60
|
+
const sortButton = target.querySelector('thead th button');
|
|
61
|
+
expect(sortButton).toBeDefined();
|
|
62
|
+
// Click to sort by name (ascending)
|
|
63
|
+
sortButton.click();
|
|
64
|
+
flushSync();
|
|
65
|
+
let firstRowName = target.querySelector('tbody tr td')?.textContent;
|
|
66
|
+
expect(firstRowName).toContain('Alice Smith');
|
|
67
|
+
// Click again to sort by name (descending)
|
|
68
|
+
sortButton.click();
|
|
69
|
+
flushSync();
|
|
70
|
+
firstRowName = target.querySelector('tbody tr td')?.textContent;
|
|
71
|
+
expect(firstRowName).toContain('Charlie Brown');
|
|
72
|
+
cleanup();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -48,15 +48,14 @@
|
|
|
48
48
|
|
|
49
49
|
let sortKey = $state<string | null>(null);
|
|
50
50
|
let sortOrder = $state<'asc' | 'desc'>('asc');
|
|
51
|
-
let
|
|
51
|
+
let customWidths = $state<Record<string, number>>({});
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const initial: Record<string, number> = {};
|
|
53
|
+
let effectiveWidths = $derived.by(() => {
|
|
54
|
+
const widths: Record<string, number> = {};
|
|
56
55
|
columns.forEach((c) => {
|
|
57
|
-
if (c.width)
|
|
56
|
+
if (c.width) widths[String(c.key)] = c.width;
|
|
58
57
|
});
|
|
59
|
-
|
|
58
|
+
return { ...widths, ...customWidths };
|
|
60
59
|
});
|
|
61
60
|
|
|
62
61
|
function handleSort(key: string) {
|
|
@@ -73,7 +72,7 @@
|
|
|
73
72
|
|
|
74
73
|
const startX = 'touches' in e ? e.touches[0].clientX : e.clientX;
|
|
75
74
|
const currentCol = columns.find((c) => String(c.key) === columnKey);
|
|
76
|
-
const currentWidth =
|
|
75
|
+
const currentWidth = effectiveWidths[columnKey] || 150;
|
|
77
76
|
const minWidth = currentCol?.minWidth || 60;
|
|
78
77
|
const maxWidth = currentCol?.maxWidth || 600;
|
|
79
78
|
|
|
@@ -81,7 +80,7 @@
|
|
|
81
80
|
const clientX = 'touches' in moveEvent ? moveEvent.touches[0].clientX : moveEvent.clientX;
|
|
82
81
|
const delta = clientX - startX;
|
|
83
82
|
const newWidth = Math.max(minWidth, Math.min(maxWidth, currentWidth + delta));
|
|
84
|
-
|
|
83
|
+
customWidths[columnKey] = newWidth;
|
|
85
84
|
}
|
|
86
85
|
|
|
87
86
|
function onEnd() {
|
|
@@ -180,8 +179,8 @@
|
|
|
180
179
|
{/if}
|
|
181
180
|
{#each columns as column}
|
|
182
181
|
{@const colKey = String(column.key)}
|
|
183
|
-
{@const widthStyle =
|
|
184
|
-
? `width: ${
|
|
182
|
+
{@const widthStyle = effectiveWidths[colKey]
|
|
183
|
+
? `width: ${effectiveWidths[colKey]}px; min-width: ${effectiveWidths[colKey]}px;`
|
|
185
184
|
: ''}
|
|
186
185
|
<th
|
|
187
186
|
class="relative px-4 py-3 {getPinClass(column.pinned)} {column.class || ''}"
|
|
@@ -291,8 +290,8 @@
|
|
|
291
290
|
{/if}
|
|
292
291
|
{#each columns as column}
|
|
293
292
|
{@const colKey = String(column.key)}
|
|
294
|
-
{@const widthStyle =
|
|
295
|
-
? `width: ${
|
|
293
|
+
{@const widthStyle = effectiveWidths[colKey]
|
|
294
|
+
? `width: ${effectiveWidths[colKey]}px; min-width: ${effectiveWidths[colKey]}px;`
|
|
296
295
|
: ''}
|
|
297
296
|
<td
|
|
298
297
|
class="px-4 py-3.5 {getPinClass(column.pinned)} {column.class || ''}"
|
|
@@ -126,7 +126,38 @@
|
|
|
126
126
|
'<circle cx="12" cy="12" r="1.5" fill="currentColor"/><circle cx="12" cy="5" r="1.5" fill="currentColor"/><circle cx="12" cy="19" r="1.5" fill="currentColor"/>',
|
|
127
127
|
user: '<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/><circle cx="12" cy="7" r="4" stroke="currentColor" stroke-width="2" fill="none"/>',
|
|
128
128
|
users:
|
|
129
|
-
'<path d="M17 21v-2a4 4 0 00-3-3.87M9 21v-2a4 4 0 013-3.87M16 3.13a4 4 0 010 7.75M8 3.13a4 4 0 000 7.75" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/>'
|
|
129
|
+
'<path d="M17 21v-2a4 4 0 00-3-3.87M9 21v-2a4 4 0 013-3.87M16 3.13a4 4 0 010 7.75M8 3.13a4 4 0 000 7.75" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/>',
|
|
130
|
+
list: '<path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>',
|
|
131
|
+
'help-circle':
|
|
132
|
+
'<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none"/><path d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3m.08 4h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
|
|
133
|
+
help: '<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none"/><path d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3m.08 4h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
|
|
134
|
+
bolt: '<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>',
|
|
135
|
+
cube: '<path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/><polyline points="3.27 6.96 12 12.01 20.73 6.96" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/><line x1="12" y1="22.08" x2="12" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
|
|
136
|
+
swatch:
|
|
137
|
+
'<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10c.88 0 1.6-.72 1.6-1.6 0-.41-.17-.79-.44-1.06-.27-.28-.44-.65-.44-1.06 0-.88.72-1.6 1.6-1.6h1.88c3.15 0 5.72-2.57 5.72-5.72C21.32 6.16 17.14 2 12 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/><circle cx="6.5" cy="11.5" r="1.5" fill="currentColor"/><circle cx="9.5" cy="7.5" r="1.5" fill="currentColor"/><circle cx="14.5" cy="7.5" r="1.5" fill="currentColor"/><circle cx="17.5" cy="11.5" r="1.5" fill="currentColor"/>',
|
|
138
|
+
star: '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>',
|
|
139
|
+
photo:
|
|
140
|
+
'<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><circle cx="8.5" cy="8.5" r="1.5" fill="currentColor"/><polyline points="21 15 16 10 5 21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>',
|
|
141
|
+
'document-text':
|
|
142
|
+
'<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/><polyline points="14 2 14 8 20 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/><line x1="8" y1="13" x2="16" y2="13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="8" y1="17" x2="13" y2="17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>',
|
|
143
|
+
'clipboard-document':
|
|
144
|
+
'<path d="M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/><rect x="8" y="2" width="8" height="4" rx="1" stroke="currentColor" stroke-width="2" fill="none"/><line x1="9" y1="12" x2="15" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="9" y1="16" x2="13" y2="16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>',
|
|
145
|
+
'arrow-path':
|
|
146
|
+
'<path d="M21.5 2v6h-6M21.34 15.57a10 10 0 11-.57-8.38l5.67-5.19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>',
|
|
147
|
+
'code-bracket':
|
|
148
|
+
'<polyline points="16 18 22 12 16 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/><polyline points="8 6 2 12 8 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>',
|
|
149
|
+
'squares-2x2':
|
|
150
|
+
'<rect x="3" y="3" width="7" height="7" rx="1" stroke="currentColor" stroke-width="2" fill="none"/><rect x="14" y="3" width="7" height="7" rx="1" stroke="currentColor" stroke-width="2" fill="none"/><rect x="14" y="14" width="7" height="7" rx="1" stroke="currentColor" stroke-width="2" fill="none"/><rect x="3" y="14" width="7" height="7" rx="1" stroke="currentColor" stroke-width="2" fill="none"/>',
|
|
151
|
+
'squares-plus':
|
|
152
|
+
'<rect x="3" y="3" width="7" height="7" rx="1" stroke="currentColor" stroke-width="2" fill="none"/><rect x="14" y="3" width="7" height="7" rx="1" stroke="currentColor" stroke-width="2" fill="none"/><rect x="3" y="14" width="7" height="7" rx="1" stroke="currentColor" stroke-width="2" fill="none"/><line x1="17.5" y1="14" x2="17.5" y2="21" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="14" y1="17.5" x2="21" y2="17.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>',
|
|
153
|
+
'arrow-top-right-on-square':
|
|
154
|
+
'<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6M15 3h6v6M10 14L21 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>',
|
|
155
|
+
sidebar:
|
|
156
|
+
'<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><line x1="9" y1="3" x2="9" y2="21" stroke="currentColor" stroke-width="2"/>',
|
|
157
|
+
'layout-top':
|
|
158
|
+
'<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><line x1="3" y1="9" x2="21" y2="9" stroke="currentColor" stroke-width="2"/>',
|
|
159
|
+
'panel-right':
|
|
160
|
+
'<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><line x1="15" y1="3" x2="15" y2="21" stroke="currentColor" stroke-width="2"/>'
|
|
130
161
|
};
|
|
131
162
|
</script>
|
|
132
163
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
variant: {
|
|
8
8
|
pulse: 'animate-pulse',
|
|
9
9
|
shimmer:
|
|
10
|
-
'before:absolute before:inset-0 before:-translate-x-full before:animate-[yaxa-
|
|
10
|
+
'before:absolute before:inset-0 before:-translate-x-full before:animate-[yaxa-shimmer-slide_1.5s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/60 before:to-transparent dark:before:via-white/20',
|
|
11
11
|
none: ''
|
|
12
12
|
},
|
|
13
13
|
shape: {
|
|
@@ -2,7 +2,7 @@ import { type VariantProps } from '../../utils/cn';
|
|
|
2
2
|
export declare const skeletonVariants: import("tailwind-variants").TVReturnType<{
|
|
3
3
|
variant: {
|
|
4
4
|
pulse: "animate-pulse";
|
|
5
|
-
shimmer: "before:absolute before:inset-0 before:-translate-x-full before:animate-[yaxa-
|
|
5
|
+
shimmer: "before:absolute before:inset-0 before:-translate-x-full before:animate-[yaxa-shimmer-slide_1.5s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/60 before:to-transparent dark:before:via-white/20";
|
|
6
6
|
none: "";
|
|
7
7
|
};
|
|
8
8
|
shape: {
|
|
@@ -14,7 +14,7 @@ export declare const skeletonVariants: import("tailwind-variants").TVReturnType<
|
|
|
14
14
|
}, undefined, "relative overflow-hidden bg-neutral-200 dark:bg-neutral-800 select-none", {
|
|
15
15
|
variant: {
|
|
16
16
|
pulse: "animate-pulse";
|
|
17
|
-
shimmer: "before:absolute before:inset-0 before:-translate-x-full before:animate-[yaxa-
|
|
17
|
+
shimmer: "before:absolute before:inset-0 before:-translate-x-full before:animate-[yaxa-shimmer-slide_1.5s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/60 before:to-transparent dark:before:via-white/20";
|
|
18
18
|
none: "";
|
|
19
19
|
};
|
|
20
20
|
shape: {
|
|
@@ -26,7 +26,7 @@ export declare const skeletonVariants: import("tailwind-variants").TVReturnType<
|
|
|
26
26
|
}, undefined, import("tailwind-variants").TVReturnTypeLike<{
|
|
27
27
|
variant: {
|
|
28
28
|
pulse: "animate-pulse";
|
|
29
|
-
shimmer: "before:absolute before:inset-0 before:-translate-x-full before:animate-[yaxa-
|
|
29
|
+
shimmer: "before:absolute before:inset-0 before:-translate-x-full before:animate-[yaxa-shimmer-slide_1.5s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/60 before:to-transparent dark:before:via-white/20";
|
|
30
30
|
none: "";
|
|
31
31
|
};
|
|
32
32
|
shape: {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { renderComponent } from '../../testing';
|
|
3
|
+
import Skeleton, { skeletonVariants } from './Skeleton.svelte';
|
|
4
|
+
describe('Skeleton Component (DOM & Variants)', () => {
|
|
5
|
+
it('generates pulse animation classes by default', () => {
|
|
6
|
+
const classes = skeletonVariants({ variant: 'pulse', shape: 'rectangle' });
|
|
7
|
+
expect(classes).toContain('animate-pulse');
|
|
8
|
+
expect(classes).toContain('rounded-lg');
|
|
9
|
+
});
|
|
10
|
+
it('generates shimmer sliding gradient classes for variant shimmer', () => {
|
|
11
|
+
const classes = skeletonVariants({ variant: 'shimmer' });
|
|
12
|
+
expect(classes).toContain('yaxa-shimmer-slide');
|
|
13
|
+
expect(classes).toContain('before:bg-gradient-to-r');
|
|
14
|
+
});
|
|
15
|
+
it('supports shape variants: circle, pill, text', () => {
|
|
16
|
+
const circle = skeletonVariants({ shape: 'circle' });
|
|
17
|
+
expect(circle).toContain('rounded-full');
|
|
18
|
+
expect(circle).toContain('aspect-square');
|
|
19
|
+
const pill = skeletonVariants({ shape: 'pill' });
|
|
20
|
+
expect(pill).toContain('rounded-full');
|
|
21
|
+
const text = skeletonVariants({ shape: 'text' });
|
|
22
|
+
expect(text).toContain('h-4');
|
|
23
|
+
});
|
|
24
|
+
it('mounts single skeleton DOM element with custom width and height', () => {
|
|
25
|
+
const { target, cleanup } = renderComponent(Skeleton, {
|
|
26
|
+
variant: 'shimmer',
|
|
27
|
+
shape: 'circle',
|
|
28
|
+
width: '48px',
|
|
29
|
+
height: '48px'
|
|
30
|
+
});
|
|
31
|
+
const el = target.querySelector('div');
|
|
32
|
+
expect(el).toBeDefined();
|
|
33
|
+
expect(el?.getAttribute('aria-hidden')).toBe('true');
|
|
34
|
+
expect(el?.style.width).toBe('48px');
|
|
35
|
+
expect(el?.style.height).toBe('48px');
|
|
36
|
+
expect(el?.className).toContain('rounded-full');
|
|
37
|
+
cleanup();
|
|
38
|
+
});
|
|
39
|
+
it('renders multiple lines for lines prop > 1', () => {
|
|
40
|
+
const { target, cleanup } = renderComponent(Skeleton, {
|
|
41
|
+
lines: 4,
|
|
42
|
+
variant: 'shimmer'
|
|
43
|
+
});
|
|
44
|
+
const container = target.querySelector('.space-y-2');
|
|
45
|
+
expect(container).toBeDefined();
|
|
46
|
+
expect(container?.children.length).toBe(4);
|
|
47
|
+
cleanup();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import Icon from '../elements/Icon.svelte';
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
|
+
id?: string;
|
|
6
7
|
checked?: boolean;
|
|
7
8
|
label?: string;
|
|
8
9
|
description?: string;
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
let {
|
|
21
|
+
id,
|
|
20
22
|
checked = $bindable(false),
|
|
21
23
|
label,
|
|
22
24
|
description,
|
|
@@ -38,6 +40,7 @@
|
|
|
38
40
|
>
|
|
39
41
|
<div class="flex h-5 items-center">
|
|
40
42
|
<input
|
|
43
|
+
id={id || name}
|
|
41
44
|
type="checkbox"
|
|
42
45
|
bind:checked
|
|
43
46
|
aria-label={ariaLabelAttr || ariaLabel || label}
|