@karbonjs/ui-svelte 0.1.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/LICENSE +21 -0
- package/package.json +30 -0
- package/src/accordion/Accordion.svelte +63 -0
- package/src/alert/AlertMessage.svelte +44 -0
- package/src/avatar/Avatar.svelte +48 -0
- package/src/badge/Badge.svelte +24 -0
- package/src/breadcrumb/Breadcrumb.svelte +34 -0
- package/src/button/Button.svelte +89 -0
- package/src/carousel/Carousel.svelte +118 -0
- package/src/data/DataTable.svelte +18 -0
- package/src/data/Pagination.svelte +45 -0
- package/src/divider/Divider.svelte +27 -0
- package/src/dropdown/Dropdown.svelte +61 -0
- package/src/form/Checkbox.svelte +51 -0
- package/src/form/ColorPicker.svelte +95 -0
- package/src/form/DatePicker.svelte +196 -0
- package/src/form/FormInput.svelte +174 -0
- package/src/form/Radio.svelte +54 -0
- package/src/form/Select.svelte +73 -0
- package/src/form/Slider.svelte +74 -0
- package/src/form/Textarea.svelte +86 -0
- package/src/form/Toggle.svelte +55 -0
- package/src/image/Image.svelte +89 -0
- package/src/image/ImgZoom.svelte +96 -0
- package/src/index.ts +71 -0
- package/src/kbd/Kbd.svelte +19 -0
- package/src/layout/Card.svelte +67 -0
- package/src/layout/EmptyState.svelte +25 -0
- package/src/layout/PageHeader.svelte +27 -0
- package/src/overlay/Dialog.svelte +135 -0
- package/src/overlay/ImgBox.svelte +174 -0
- package/src/overlay/Modal.svelte +98 -0
- package/src/overlay/Toast.svelte +92 -0
- package/src/progress/Progress.svelte +50 -0
- package/src/skeleton/Skeleton.svelte +50 -0
- package/src/tabs/Tabs.svelte +59 -0
- package/src/tooltip/Tooltip.svelte +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 KarbonJS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@karbonjs/ui-svelte",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Karbon UI components for Svelte 5",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"svelte": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"svelte": "./src/index.ts",
|
|
11
|
+
"types": "./src/index.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@karbonjs/ui-core": "0.1.0"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"svelte": "^5.0.0"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "echo 'svelte components ship as source'"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
import type { AccordionItem } from '@karbonjs/ui-core'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
items: AccordionItem[]
|
|
7
|
+
multiple?: boolean
|
|
8
|
+
class?: string
|
|
9
|
+
children?: Snippet<[{ item: AccordionItem; index: number }]>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
items,
|
|
14
|
+
multiple = false,
|
|
15
|
+
class: className = '',
|
|
16
|
+
children
|
|
17
|
+
}: Props = $props()
|
|
18
|
+
|
|
19
|
+
let openIds = $state<Set<string>>(new Set())
|
|
20
|
+
|
|
21
|
+
function toggle(id: string) {
|
|
22
|
+
const next = new Set(openIds)
|
|
23
|
+
if (next.has(id)) {
|
|
24
|
+
next.delete(id)
|
|
25
|
+
} else {
|
|
26
|
+
if (!multiple) next.clear()
|
|
27
|
+
next.add(id)
|
|
28
|
+
}
|
|
29
|
+
openIds = next
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<div class="rounded-xl border border-[var(--karbon-border,rgba(0,0,0,0.07))] overflow-hidden divide-y divide-[var(--karbon-border,rgba(0,0,0,0.07))] {className}">
|
|
34
|
+
{#each items as item, index}
|
|
35
|
+
<div>
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
onclick={() => { if (!item.disabled) toggle(item.id) }}
|
|
39
|
+
disabled={item.disabled}
|
|
40
|
+
class="w-full flex items-center justify-between px-4 py-3 text-sm font-medium text-left transition-colors
|
|
41
|
+
text-[var(--karbon-text,#1a1635)] hover:bg-[var(--karbon-nav-hover-bg,rgba(0,0,0,0.04))]
|
|
42
|
+
disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
|
|
43
|
+
>
|
|
44
|
+
<span>{item.title}</span>
|
|
45
|
+
<svg
|
|
46
|
+
xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
|
|
47
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
|
48
|
+
class="shrink-0 text-[var(--karbon-text-4,#b5b2cc)] transition-transform duration-200 {openIds.has(item.id) ? 'rotate-180' : ''}"
|
|
49
|
+
><path d="m6 9 6 6 6-6"/></svg>
|
|
50
|
+
</button>
|
|
51
|
+
|
|
52
|
+
{#if openIds.has(item.id)}
|
|
53
|
+
<div class="px-4 pb-3 text-sm text-[var(--karbon-text-2,#5a567e)]">
|
|
54
|
+
{#if children}
|
|
55
|
+
{@render children({ item, index })}
|
|
56
|
+
{:else if item.content}
|
|
57
|
+
<p>{item.content}</p>
|
|
58
|
+
{/if}
|
|
59
|
+
</div>
|
|
60
|
+
{/if}
|
|
61
|
+
</div>
|
|
62
|
+
{/each}
|
|
63
|
+
</div>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
import type { AlertType } from '@karbonjs/ui-core'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
type?: AlertType
|
|
7
|
+
message?: string
|
|
8
|
+
class?: string
|
|
9
|
+
children?: Snippet
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
type = 'error',
|
|
14
|
+
message = '',
|
|
15
|
+
class: className = '',
|
|
16
|
+
children
|
|
17
|
+
}: Props = $props()
|
|
18
|
+
|
|
19
|
+
const variants: Record<string, string> = {
|
|
20
|
+
error: 'bg-red-500/8 border border-red-500/20 text-red-400',
|
|
21
|
+
success: 'bg-green-500/8 border border-green-500/20 text-green-400',
|
|
22
|
+
warning: 'bg-amber-500/8 border border-amber-500/20 text-amber-300',
|
|
23
|
+
info: 'bg-blue-500/8 border border-blue-500/20 text-blue-400'
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
{#if message || children}
|
|
28
|
+
<div class="flex items-center gap-2.5 px-4 py-3 rounded-[0.625rem] text-[0.825rem] font-medium {variants[type]} {className}">
|
|
29
|
+
{#if type === 'error'}
|
|
30
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0"><circle cx="12" cy="12" r="10"/><line x1="12" x2="12" y1="8" y2="12"/><line x1="12" x2="12.01" y1="16" y2="16"/></svg>
|
|
31
|
+
{:else if type === 'success'}
|
|
32
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/></svg>
|
|
33
|
+
{:else if type === 'warning'}
|
|
34
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
|
|
35
|
+
{:else}
|
|
36
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
|
|
37
|
+
{/if}
|
|
38
|
+
{#if children}
|
|
39
|
+
{@render children()}
|
|
40
|
+
{:else}
|
|
41
|
+
<span>{message}</span>
|
|
42
|
+
{/if}
|
|
43
|
+
</div>
|
|
44
|
+
{/if}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { AvatarSize } from '@karbonjs/ui-core'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
src?: string
|
|
6
|
+
alt?: string
|
|
7
|
+
name?: string
|
|
8
|
+
size?: AvatarSize
|
|
9
|
+
class?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
src = '',
|
|
14
|
+
alt = '',
|
|
15
|
+
name = '',
|
|
16
|
+
size = 'md',
|
|
17
|
+
class: className = ''
|
|
18
|
+
}: Props = $props()
|
|
19
|
+
|
|
20
|
+
let errored = $state(false)
|
|
21
|
+
|
|
22
|
+
const sizeClasses: Record<string, string> = {
|
|
23
|
+
xs: 'w-6 h-6 text-[10px]',
|
|
24
|
+
sm: 'w-8 h-8 text-xs',
|
|
25
|
+
md: 'w-10 h-10 text-sm',
|
|
26
|
+
lg: 'w-14 h-14 text-lg',
|
|
27
|
+
xl: 'w-20 h-20 text-2xl'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const initials = $derived(
|
|
31
|
+
(name || alt || 'A').split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase()
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
const showImage = $derived(src && !errored)
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<div class="relative inline-flex items-center justify-center shrink-0 rounded-full overflow-hidden bg-[var(--karbon-bg-2,#e8e6f0)] text-[var(--karbon-text-2,#5a567e)] font-semibold {sizeClasses[size]} {className}">
|
|
38
|
+
{#if showImage}
|
|
39
|
+
<img
|
|
40
|
+
{src}
|
|
41
|
+
alt={alt || name}
|
|
42
|
+
onerror={() => errored = true}
|
|
43
|
+
class="w-full h-full object-cover"
|
|
44
|
+
/>
|
|
45
|
+
{:else}
|
|
46
|
+
<span>{initials}</span>
|
|
47
|
+
{/if}
|
|
48
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
import type { BadgeVariant } from '@karbonjs/ui-core'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
variant?: BadgeVariant
|
|
7
|
+
class?: string
|
|
8
|
+
children: Snippet
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { variant = 'default', class: className = '', children }: Props = $props()
|
|
12
|
+
|
|
13
|
+
const variants: Record<string, string> = {
|
|
14
|
+
default: 'bg-[var(--karbon-bg-2,rgba(255,255,255,0.08))] text-[var(--karbon-text-2,#a1a1aa)]',
|
|
15
|
+
success: 'bg-emerald-500/15 text-emerald-500',
|
|
16
|
+
warning: 'bg-amber-500/15 text-amber-500',
|
|
17
|
+
danger: 'bg-red-500/15 text-red-500',
|
|
18
|
+
info: 'bg-blue-500/15 text-blue-500'
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium {variants[variant]} {className}">
|
|
23
|
+
{@render children()}
|
|
24
|
+
</span>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { BreadcrumbItem } from '@karbonjs/ui-core'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
items: BreadcrumbItem[]
|
|
6
|
+
separator?: string
|
|
7
|
+
class?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
items,
|
|
12
|
+
separator = '/',
|
|
13
|
+
class: className = ''
|
|
14
|
+
}: Props = $props()
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<nav aria-label="Breadcrumb" class={className}>
|
|
18
|
+
<ol class="flex items-center gap-1.5 text-sm">
|
|
19
|
+
{#each items as item, i}
|
|
20
|
+
{#if i > 0}
|
|
21
|
+
<li class="text-[var(--karbon-text-4,#b5b2cc)] select-none" aria-hidden="true">{separator}</li>
|
|
22
|
+
{/if}
|
|
23
|
+
<li>
|
|
24
|
+
{#if item.href && i < items.length - 1}
|
|
25
|
+
<a href={item.href} class="text-[var(--karbon-text-3,#8e8aae)] hover:text-[var(--karbon-text,#1a1635)] transition-colors">
|
|
26
|
+
{item.label}
|
|
27
|
+
</a>
|
|
28
|
+
{:else}
|
|
29
|
+
<span class="text-[var(--karbon-text,#1a1635)] font-medium">{item.label}</span>
|
|
30
|
+
{/if}
|
|
31
|
+
</li>
|
|
32
|
+
{/each}
|
|
33
|
+
</ol>
|
|
34
|
+
</nav>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
import type { ButtonVariant, ButtonSize } from '@karbonjs/ui-core'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
variant?: ButtonVariant
|
|
7
|
+
size?: ButtonSize
|
|
8
|
+
type?: 'button' | 'submit'
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
loading?: boolean
|
|
11
|
+
loadingText?: string
|
|
12
|
+
arrow?: boolean
|
|
13
|
+
fullWidth?: boolean
|
|
14
|
+
class?: string
|
|
15
|
+
onclick?: () => void
|
|
16
|
+
children: Snippet
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
variant = 'primary',
|
|
21
|
+
size = 'md',
|
|
22
|
+
type = 'button',
|
|
23
|
+
disabled = false,
|
|
24
|
+
loading = false,
|
|
25
|
+
loadingText = '',
|
|
26
|
+
arrow = false,
|
|
27
|
+
fullWidth = false,
|
|
28
|
+
class: className = '',
|
|
29
|
+
onclick,
|
|
30
|
+
children
|
|
31
|
+
}: Props = $props()
|
|
32
|
+
|
|
33
|
+
const isDisabled = $derived(disabled || loading)
|
|
34
|
+
|
|
35
|
+
const variantClasses: Record<string, string> = {
|
|
36
|
+
primary: 'bg-[var(--karbon-primary)] text-white hover:bg-[var(--karbon-primary-hover)] focus:ring-[var(--karbon-primary)]',
|
|
37
|
+
secondary: 'bg-[var(--karbon-bg-2)] text-[var(--karbon-text-2)] hover:bg-[var(--karbon-border)] focus:ring-[var(--karbon-primary)]',
|
|
38
|
+
danger: 'bg-[var(--karbon-danger)] text-white hover:bg-red-600 focus:ring-[var(--karbon-danger)]',
|
|
39
|
+
ghost: 'text-[var(--karbon-text-3)] hover:bg-[var(--karbon-nav-hover-bg)] focus:ring-[var(--karbon-primary)]',
|
|
40
|
+
outline: 'border border-[var(--karbon-border)] text-[var(--karbon-text-2)] hover:bg-[var(--karbon-nav-hover-bg)] focus:ring-[var(--karbon-primary)]'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const sizeClasses: Record<string, string> = {
|
|
44
|
+
sm: 'px-3 py-1.5 text-sm',
|
|
45
|
+
md: 'px-4 py-2 text-sm',
|
|
46
|
+
lg: 'px-6 py-3 text-base'
|
|
47
|
+
}
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<button
|
|
51
|
+
{type}
|
|
52
|
+
disabled={isDisabled}
|
|
53
|
+
{onclick}
|
|
54
|
+
class="
|
|
55
|
+
inline-flex items-center justify-center font-semibold rounded-lg
|
|
56
|
+
transition-all duration-300 ease-out
|
|
57
|
+
focus:outline-none focus:ring-2 focus:ring-offset-0
|
|
58
|
+
cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed
|
|
59
|
+
{variantClasses[variant]}
|
|
60
|
+
{arrow || fullWidth ? 'relative overflow-hidden py-3 md:py-3.5 px-4 text-[0.8125rem] md:text-sm' : sizeClasses[size]}
|
|
61
|
+
{fullWidth ? 'w-full' : ''}
|
|
62
|
+
{arrow ? 'group' : ''}
|
|
63
|
+
active:enabled:scale-[0.97]
|
|
64
|
+
{className}
|
|
65
|
+
"
|
|
66
|
+
>
|
|
67
|
+
{#if arrow}
|
|
68
|
+
<span class="flex items-center gap-2 transition-transform duration-300 group-hover:enabled:-translate-x-2.5">
|
|
69
|
+
{#if loading}
|
|
70
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="animate-spin"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
|
|
71
|
+
{#if loadingText}<span>{loadingText}</span>{/if}
|
|
72
|
+
{:else}
|
|
73
|
+
{@render children()}
|
|
74
|
+
{/if}
|
|
75
|
+
</span>
|
|
76
|
+
{#if !loading}
|
|
77
|
+
<span class="absolute right-4 flex items-center opacity-0 -translate-x-2 transition-all duration-300 group-hover:enabled:opacity-100 group-hover:enabled:translate-x-0">
|
|
78
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
|
|
79
|
+
</span>
|
|
80
|
+
{/if}
|
|
81
|
+
{:else}
|
|
82
|
+
{#if loading}
|
|
83
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="animate-spin mr-2"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
|
|
84
|
+
{#if loadingText}<span>{loadingText}</span>{:else}{@render children()}{/if}
|
|
85
|
+
{:else}
|
|
86
|
+
{@render children()}
|
|
87
|
+
{/if}
|
|
88
|
+
{/if}
|
|
89
|
+
</button>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
images?: string[]
|
|
6
|
+
autoplay?: number
|
|
7
|
+
loop?: boolean
|
|
8
|
+
arrows?: boolean
|
|
9
|
+
indicators?: boolean
|
|
10
|
+
class?: string
|
|
11
|
+
children?: Snippet
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
images = [],
|
|
16
|
+
autoplay = 0,
|
|
17
|
+
loop = false,
|
|
18
|
+
arrows = true,
|
|
19
|
+
indicators = true,
|
|
20
|
+
class: className = '',
|
|
21
|
+
children
|
|
22
|
+
}: Props = $props()
|
|
23
|
+
|
|
24
|
+
let current = $state(0)
|
|
25
|
+
let timer: ReturnType<typeof setInterval> | null = null
|
|
26
|
+
|
|
27
|
+
const hasSlots = $derived(!!children)
|
|
28
|
+
const total = $derived(hasSlots ? 0 : images.length)
|
|
29
|
+
const hasPrev = $derived(loop || current > 0)
|
|
30
|
+
const hasNext = $derived(loop || current < total - 1)
|
|
31
|
+
|
|
32
|
+
function goTo(i: number) {
|
|
33
|
+
if (total === 0) return
|
|
34
|
+
if (loop) {
|
|
35
|
+
current = ((i % total) + total) % total
|
|
36
|
+
} else {
|
|
37
|
+
current = Math.max(0, Math.min(i, total - 1))
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function prev() { goTo(current - 1) }
|
|
42
|
+
function next() { goTo(current + 1) }
|
|
43
|
+
|
|
44
|
+
function startAutoplay() {
|
|
45
|
+
if (autoplay > 0 && total > 1) {
|
|
46
|
+
timer = setInterval(next, autoplay)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function stopAutoplay() {
|
|
51
|
+
if (timer) { clearInterval(timer); timer = null }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
$effect(() => {
|
|
55
|
+
startAutoplay()
|
|
56
|
+
return stopAutoplay
|
|
57
|
+
})
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
61
|
+
<div
|
|
62
|
+
class="relative overflow-hidden rounded-xl {className}"
|
|
63
|
+
onmouseenter={stopAutoplay}
|
|
64
|
+
onmouseleave={startAutoplay}
|
|
65
|
+
>
|
|
66
|
+
{#if hasSlots}
|
|
67
|
+
{@render children()}
|
|
68
|
+
{:else}
|
|
69
|
+
<div
|
|
70
|
+
class="flex transition-transform duration-500 ease-out"
|
|
71
|
+
style="transform: translateX(-{current * 100}%)"
|
|
72
|
+
>
|
|
73
|
+
{#each images as src, i}
|
|
74
|
+
<div class="w-full shrink-0">
|
|
75
|
+
<img
|
|
76
|
+
{src}
|
|
77
|
+
alt="Slide {i + 1}"
|
|
78
|
+
class="w-full h-full object-cover"
|
|
79
|
+
loading="lazy"
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
{/each}
|
|
83
|
+
</div>
|
|
84
|
+
{/if}
|
|
85
|
+
|
|
86
|
+
{#if arrows && total > 1}
|
|
87
|
+
{#if hasPrev}
|
|
88
|
+
<button
|
|
89
|
+
onclick={prev}
|
|
90
|
+
aria-label="Slide précédent"
|
|
91
|
+
class="absolute left-3 top-1/2 -translate-y-1/2 z-10 rounded-full p-2 bg-black/30 text-white/80 hover:bg-black/50 hover:text-white transition-colors cursor-pointer"
|
|
92
|
+
>
|
|
93
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
|
94
|
+
</button>
|
|
95
|
+
{/if}
|
|
96
|
+
{#if hasNext}
|
|
97
|
+
<button
|
|
98
|
+
onclick={next}
|
|
99
|
+
aria-label="Slide suivant"
|
|
100
|
+
class="absolute right-3 top-1/2 -translate-y-1/2 z-10 rounded-full p-2 bg-black/30 text-white/80 hover:bg-black/50 hover:text-white transition-colors cursor-pointer"
|
|
101
|
+
>
|
|
102
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
|
|
103
|
+
</button>
|
|
104
|
+
{/if}
|
|
105
|
+
{/if}
|
|
106
|
+
|
|
107
|
+
{#if indicators && total > 1}
|
|
108
|
+
<div class="absolute bottom-3 left-1/2 -translate-x-1/2 z-10 flex items-center gap-1.5">
|
|
109
|
+
{#each images as _, i}
|
|
110
|
+
<button
|
|
111
|
+
onclick={() => goTo(i)}
|
|
112
|
+
aria-label="Aller au slide {i + 1}"
|
|
113
|
+
class="w-2 h-2 rounded-full transition-all cursor-pointer {i === current ? 'bg-white w-4' : 'bg-white/40 hover:bg-white/60'}"
|
|
114
|
+
></button>
|
|
115
|
+
{/each}
|
|
116
|
+
</div>
|
|
117
|
+
{/if}
|
|
118
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
class?: string
|
|
6
|
+
children: Snippet
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let { class: className = '', children }: Props = $props()
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<div class="overflow-hidden rounded-xl bg-[var(--karbon-bg-card,#fff)] border border-[var(--karbon-border,rgba(0,0,0,0.07))] {className}">
|
|
13
|
+
<div class="overflow-x-auto">
|
|
14
|
+
<table class="min-w-full [&_thead_tr]:bg-[var(--karbon-bg-2,#e8e6f0)] [&_thead_tr]:border-b [&_thead_tr]:border-[var(--karbon-border,rgba(0,0,0,0.07))] [&_thead_th]:text-[var(--karbon-text-3,#8e8aae)] [&_thead_th]:text-xs [&_thead_th]:font-semibold [&_thead_th]:uppercase [&_thead_th]:tracking-wider [&_thead_th]:px-4 [&_thead_th]:py-3 [&_thead_th]:text-left [&_tbody_tr]:border-b [&_tbody_tr]:border-[var(--karbon-border,rgba(0,0,0,0.07))] [&_tbody_tr]:transition-colors [&_tbody_tr]:duration-150 [&_tbody_tr:last-child]:border-b-0 [&_tbody_tr:hover]:bg-[var(--karbon-nav-hover-bg,rgba(0,0,0,0.04))] [&_td]:px-4 [&_td]:py-3 [&_td]:text-sm [&_td]:text-[var(--karbon-text-2,#5a567e)]">
|
|
15
|
+
{@render children()}
|
|
16
|
+
</table>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
page: number
|
|
4
|
+
total: number
|
|
5
|
+
perPage: number
|
|
6
|
+
baseUrl: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let { page, total, perPage, baseUrl }: Props = $props()
|
|
10
|
+
|
|
11
|
+
const totalPages = $derived(Math.ceil(total / perPage))
|
|
12
|
+
const pages = $derived((() => {
|
|
13
|
+
const p: number[] = []
|
|
14
|
+
const start = Math.max(1, page - 2)
|
|
15
|
+
const end = Math.min(totalPages, page + 2)
|
|
16
|
+
for (let i = start; i <= end; i++) p.push(i)
|
|
17
|
+
return p
|
|
18
|
+
})())
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
{#if totalPages > 1}
|
|
22
|
+
<nav class="flex items-center gap-1">
|
|
23
|
+
{#if page > 1}
|
|
24
|
+
<a href="{baseUrl}?page={page - 1}" aria-label="Page précédente" class="rounded-lg p-2 transition-colors text-[var(--karbon-text-3,#8e8aae)] hover:bg-[var(--karbon-nav-hover-bg,rgba(0,0,0,0.04))] hover:text-[var(--karbon-text,#1a1635)]">
|
|
25
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
|
26
|
+
</a>
|
|
27
|
+
{/if}
|
|
28
|
+
|
|
29
|
+
{#each pages as p}
|
|
30
|
+
{#if p === page}
|
|
31
|
+
<span class="rounded-lg bg-[var(--karbon-primary)] px-3 py-1.5 text-sm font-medium text-white">{p}</span>
|
|
32
|
+
{:else}
|
|
33
|
+
<a href="{baseUrl}?page={p}" class="rounded-lg px-3 py-1.5 text-sm transition-colors text-[var(--karbon-text-2,#5a567e)] hover:bg-[var(--karbon-nav-hover-bg,rgba(0,0,0,0.04))] hover:text-[var(--karbon-text,#1a1635)]">{p}</a>
|
|
34
|
+
{/if}
|
|
35
|
+
{/each}
|
|
36
|
+
|
|
37
|
+
{#if page < totalPages}
|
|
38
|
+
<a href="{baseUrl}?page={page + 1}" aria-label="Page suivante" class="rounded-lg p-2 transition-colors text-[var(--karbon-text-3,#8e8aae)] hover:bg-[var(--karbon-nav-hover-bg,rgba(0,0,0,0.04))] hover:text-[var(--karbon-text,#1a1635)]">
|
|
39
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
|
|
40
|
+
</a>
|
|
41
|
+
{/if}
|
|
42
|
+
|
|
43
|
+
<span class="ml-2 text-sm text-[var(--karbon-text-4,#b5b2cc)]">{total} résultats</span>
|
|
44
|
+
</nav>
|
|
45
|
+
{/if}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { DividerDirection } from '@karbonjs/ui-core'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
direction?: DividerDirection
|
|
6
|
+
label?: string
|
|
7
|
+
class?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
direction = 'horizontal',
|
|
12
|
+
label = '',
|
|
13
|
+
class: className = ''
|
|
14
|
+
}: Props = $props()
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
{#if direction === 'vertical'}
|
|
18
|
+
<div class="inline-block w-px self-stretch bg-[var(--karbon-border,rgba(0,0,0,0.07))] {className}"></div>
|
|
19
|
+
{:else if label}
|
|
20
|
+
<div class="flex items-center gap-3 {className}">
|
|
21
|
+
<div class="flex-1 h-px bg-[var(--karbon-border,rgba(0,0,0,0.07))]"></div>
|
|
22
|
+
<span class="text-xs text-[var(--karbon-text-4,#b5b2cc)] font-medium shrink-0">{label}</span>
|
|
23
|
+
<div class="flex-1 h-px bg-[var(--karbon-border,rgba(0,0,0,0.07))]"></div>
|
|
24
|
+
</div>
|
|
25
|
+
{:else}
|
|
26
|
+
<div class="w-full h-px bg-[var(--karbon-border,rgba(0,0,0,0.07))] {className}"></div>
|
|
27
|
+
{/if}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
import type { DropdownEntry, DropdownAlign } from '@karbonjs/ui-core'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
items: DropdownEntry[]
|
|
7
|
+
align?: DropdownAlign
|
|
8
|
+
class?: string
|
|
9
|
+
trigger: Snippet
|
|
10
|
+
onselect?: (value: string) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
items,
|
|
15
|
+
align = 'left',
|
|
16
|
+
class: className = '',
|
|
17
|
+
trigger,
|
|
18
|
+
onselect
|
|
19
|
+
}: Props = $props()
|
|
20
|
+
|
|
21
|
+
let open = $state(false)
|
|
22
|
+
|
|
23
|
+
function handleSelect(item: DropdownEntry) {
|
|
24
|
+
if ('divider' in item) return
|
|
25
|
+
if (item.disabled) return
|
|
26
|
+
onselect?.(item.value ?? item.label)
|
|
27
|
+
open = false
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<svelte:window onclick={() => open = false} />
|
|
32
|
+
|
|
33
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
34
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
35
|
+
<div class="relative inline-block {className}" onclick={(e) => e.stopPropagation()}>
|
|
36
|
+
<div onclick={() => open = !open} class="cursor-pointer">
|
|
37
|
+
{@render trigger()}
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
{#if open}
|
|
41
|
+
<div class="absolute z-50 mt-1 min-w-[12rem] rounded-xl border border-[var(--karbon-border,rgba(0,0,0,0.07))] bg-[var(--karbon-bg-card,#fff)] shadow-xl py-1
|
|
42
|
+
{align === 'right' ? 'right-0' : 'left-0'}">
|
|
43
|
+
{#each items as item}
|
|
44
|
+
{#if 'divider' in item}
|
|
45
|
+
<div class="my-1 border-t border-[var(--karbon-border,rgba(0,0,0,0.07))]"></div>
|
|
46
|
+
{:else}
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
onclick={() => handleSelect(item)}
|
|
50
|
+
disabled={item.disabled}
|
|
51
|
+
class="w-full flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors
|
|
52
|
+
{item.danger ? 'text-red-400 hover:bg-red-500/8' : 'text-[var(--karbon-text,#1a1635)] hover:bg-[var(--karbon-nav-hover-bg,rgba(0,0,0,0.04))]'}
|
|
53
|
+
disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
|
|
54
|
+
>
|
|
55
|
+
{item.label}
|
|
56
|
+
</button>
|
|
57
|
+
{/if}
|
|
58
|
+
{/each}
|
|
59
|
+
</div>
|
|
60
|
+
{/if}
|
|
61
|
+
</div>
|