@karbonjs/ui-svelte 0.2.5 → 0.3.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/package.json +3 -2
- package/src/accordion/Accordion.svelte +198 -23
- package/src/alert/AlertMessage.svelte +114 -20
- package/src/avatar/Avatar.svelte +16 -2
- package/src/badge/Badge.svelte +99 -10
- package/src/breadcrumb/Breadcrumb.svelte +124 -13
- package/src/button/Button.svelte +106 -21
- package/src/button/ButtonBrand.svelte +229 -0
- package/src/carousel/Carousel.svelte +161 -28
- package/src/code/CodeBlock.svelte +323 -0
- package/src/data/DataTable.svelte +319 -8
- package/src/data/Pagination.svelte +168 -28
- package/src/divider/Divider.svelte +91 -10
- package/src/dropdown/Dropdown.svelte +171 -27
- package/src/editor/RichTextEditor.svelte +861 -107
- package/src/form/Checkbox.svelte +110 -18
- package/src/form/ColorPicker.svelte +28 -16
- package/src/form/DatePicker.svelte +20 -10
- package/src/form/{FormInput.svelte → Input.svelte} +41 -14
- package/src/form/Radio.svelte +86 -18
- package/src/form/Select.svelte +246 -33
- package/src/form/Slider.svelte +22 -7
- package/src/form/Textarea.svelte +53 -10
- package/src/form/Toggle.svelte +72 -18
- package/src/image/Image.svelte +6 -4
- package/src/image/ImageCompare.svelte +182 -0
- package/src/image/ImgZoom.svelte +131 -49
- package/src/index.ts +7 -1
- package/src/kbd/Kbd.svelte +4 -3
- package/src/layout/Card.svelte +12 -6
- package/src/layout/EmptyState.svelte +75 -8
- package/src/layout/PageHeader.svelte +111 -11
- package/src/overlay/Dialog.svelte +147 -67
- package/src/overlay/ImgBox.svelte +125 -21
- package/src/overlay/Modal.svelte +110 -28
- package/src/overlay/Toast.svelte +152 -55
- package/src/progress/Progress.svelte +137 -26
- package/src/skeleton/Skeleton.svelte +6 -4
- package/src/tabs/Tabs.svelte +133 -22
- package/src/tooltip/Tooltip.svelte +110 -20
package/src/tabs/Tabs.svelte
CHANGED
|
@@ -1,59 +1,170 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte'
|
|
3
|
-
import type {
|
|
3
|
+
import type { ButtonColor } from '@karbonjs/ui-core'
|
|
4
|
+
|
|
5
|
+
interface TabItem {
|
|
6
|
+
id: string
|
|
7
|
+
label: string
|
|
8
|
+
icon?: string
|
|
9
|
+
badge?: string | number
|
|
10
|
+
disabled?: boolean
|
|
11
|
+
}
|
|
4
12
|
|
|
5
13
|
interface Props {
|
|
6
14
|
tabs: TabItem[]
|
|
7
15
|
active?: string
|
|
16
|
+
variant?: 'underline' | 'pills' | 'bordered' | 'segment'
|
|
17
|
+
color?: ButtonColor
|
|
18
|
+
size?: 'sm' | 'md' | 'lg'
|
|
19
|
+
fullWidth?: boolean
|
|
20
|
+
vertical?: boolean
|
|
8
21
|
class?: string
|
|
22
|
+
classes?: { root?: string, list?: string, tab?: string, panel?: string, indicator?: string }
|
|
9
23
|
onchange?: (id: string) => void
|
|
10
|
-
|
|
24
|
+
panel?: Snippet<[string]>
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
let {
|
|
14
28
|
tabs,
|
|
15
29
|
active = $bindable(tabs[0]?.id ?? ''),
|
|
30
|
+
variant = 'underline',
|
|
31
|
+
color,
|
|
32
|
+
size = 'md',
|
|
33
|
+
fullWidth = false,
|
|
34
|
+
vertical = false,
|
|
16
35
|
class: className = '',
|
|
36
|
+
classes = {},
|
|
17
37
|
onchange,
|
|
18
|
-
|
|
38
|
+
panel
|
|
19
39
|
}: Props = $props()
|
|
20
40
|
|
|
41
|
+
let listEl: HTMLElement
|
|
42
|
+
|
|
43
|
+
const accent = $derived(color ? `var(--karbon-${color}-500)` : 'var(--karbon-primary)')
|
|
44
|
+
const accentLight = $derived(color ? `var(--karbon-${color}-400)` : 'var(--karbon-primary)')
|
|
45
|
+
|
|
46
|
+
const sizeMap = {
|
|
47
|
+
sm: { px: 'px-3', py: 'py-1.5', text: 'text-xs', badge: 'text-[9px] px-1 py-px', gap: 'gap-1.5' },
|
|
48
|
+
md: { px: 'px-4', py: 'py-2.5', text: 'text-sm', badge: 'text-[10px] px-1.5 py-px', gap: 'gap-2' },
|
|
49
|
+
lg: { px: 'px-5', py: 'py-3', text: 'text-base', badge: 'text-xs px-1.5 py-0.5', gap: 'gap-2' },
|
|
50
|
+
}
|
|
51
|
+
const s = $derived(sizeMap[size])
|
|
52
|
+
|
|
53
|
+
function sanitizeSvg(html: string): string {
|
|
54
|
+
return html.replace(/on\w+\s*=/gi, '').replace(/<script/gi, '<script')
|
|
55
|
+
}
|
|
56
|
+
|
|
21
57
|
function select(id: string) {
|
|
22
58
|
active = id
|
|
23
59
|
onchange?.(id)
|
|
24
60
|
}
|
|
61
|
+
|
|
62
|
+
function tabStyle(isActive: boolean): string {
|
|
63
|
+
switch (variant) {
|
|
64
|
+
case 'underline':
|
|
65
|
+
return isActive
|
|
66
|
+
? `color:${accent};`
|
|
67
|
+
: 'color:var(--karbon-text-3);'
|
|
68
|
+
case 'pills':
|
|
69
|
+
return isActive
|
|
70
|
+
? `background:${accent};color:white;`
|
|
71
|
+
: 'color:var(--karbon-text-3);background:transparent;'
|
|
72
|
+
case 'bordered':
|
|
73
|
+
return isActive
|
|
74
|
+
? `background:var(--karbon-bg-card);color:${accent};border-color:var(--karbon-border);border-bottom-color:var(--karbon-bg-card);`
|
|
75
|
+
: 'color:var(--karbon-text-3);border-color:transparent;'
|
|
76
|
+
case 'segment':
|
|
77
|
+
return isActive
|
|
78
|
+
? `background:var(--karbon-bg-card);color:${accent};box-shadow:0 1px 3px rgba(0,0,0,0.1);`
|
|
79
|
+
: 'color:var(--karbon-text-3);background:transparent;'
|
|
80
|
+
default: return ''
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function listStyle(): string {
|
|
85
|
+
switch (variant) {
|
|
86
|
+
case 'underline': return `border-bottom:1px solid var(--karbon-border);`
|
|
87
|
+
case 'pills': return ''
|
|
88
|
+
case 'bordered': return `border-bottom:1px solid var(--karbon-border);`
|
|
89
|
+
case 'segment': return `background:var(--karbon-bg-2);padding:3px;border-radius:0.625rem;`
|
|
90
|
+
default: return ''
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function tabShapeClass(): string {
|
|
95
|
+
switch (variant) {
|
|
96
|
+
case 'pills': return 'rounded-lg'
|
|
97
|
+
case 'bordered': return 'rounded-t-lg border border-b-0'
|
|
98
|
+
case 'segment': return 'rounded-lg'
|
|
99
|
+
default: return ''
|
|
100
|
+
}
|
|
101
|
+
}
|
|
25
102
|
</script>
|
|
26
103
|
|
|
27
|
-
<div class={className}>
|
|
28
|
-
|
|
104
|
+
<div class="{vertical ? 'flex gap-4' : ''} {classes?.root ?? className}">
|
|
105
|
+
<!-- Tab list -->
|
|
106
|
+
<div
|
|
107
|
+
bind:this={listEl}
|
|
108
|
+
class="{vertical ? 'flex flex-col shrink-0' : 'flex'} {fullWidth && !vertical ? '[&>*]:flex-1' : ''} {s.gap} {classes?.list ?? ''}"
|
|
109
|
+
style={listStyle()}
|
|
110
|
+
role="tablist"
|
|
111
|
+
aria-orientation={vertical ? 'vertical' : 'horizontal'}
|
|
112
|
+
>
|
|
29
113
|
{#each tabs as tab}
|
|
114
|
+
{@const isActive = active === tab.id}
|
|
30
115
|
<button
|
|
31
116
|
type="button"
|
|
32
117
|
role="tab"
|
|
33
|
-
aria-selected={
|
|
118
|
+
aria-selected={isActive}
|
|
34
119
|
onclick={() => { if (!tab.disabled) select(tab.id) }}
|
|
35
120
|
disabled={tab.disabled}
|
|
36
|
-
class="px
|
|
37
|
-
disabled:opacity-
|
|
38
|
-
{
|
|
39
|
-
|
|
40
|
-
|
|
121
|
+
class="relative {s.px} {s.py} {s.text} font-medium transition-all cursor-pointer
|
|
122
|
+
disabled:opacity-30 disabled:cursor-not-allowed
|
|
123
|
+
{tabShapeClass()}
|
|
124
|
+
{fullWidth ? 'text-center' : ''}
|
|
125
|
+
inline-flex items-center {s.gap} whitespace-nowrap
|
|
126
|
+
{classes?.tab ?? ''}"
|
|
127
|
+
style={tabStyle(isActive)}
|
|
128
|
+
onmouseenter={(e) => { if (!isActive && !tab.disabled) (e.currentTarget as HTMLElement).style.color = 'var(--karbon-text-2)' }}
|
|
129
|
+
onmouseleave={(e) => { if (!isActive) (e.currentTarget as HTMLElement).style.cssText = tabStyle(isActive) }}
|
|
41
130
|
>
|
|
42
|
-
{tab.
|
|
43
|
-
|
|
44
|
-
|
|
131
|
+
{#if tab.icon}
|
|
132
|
+
<span class="shrink-0">{@html sanitizeSvg(tab.icon)}</span>
|
|
133
|
+
{/if}
|
|
134
|
+
<span>{tab.label}</span>
|
|
135
|
+
{#if tab.badge != null}
|
|
136
|
+
<span
|
|
137
|
+
class="rounded-full font-semibold {s.badge}"
|
|
138
|
+
style="background:{isActive ? `color-mix(in srgb,${accent} 20%,transparent)` : 'var(--karbon-bg-2)'};color:{isActive ? accent : 'var(--karbon-text-3)'};"
|
|
139
|
+
>{tab.badge}</span>
|
|
140
|
+
{/if}
|
|
141
|
+
|
|
142
|
+
<!-- Underline indicator -->
|
|
143
|
+
{#if variant === 'underline' && isActive}
|
|
144
|
+
<span
|
|
145
|
+
class="absolute {vertical ? 'right-0 top-0 bottom-0 w-0.5' : 'bottom-0 left-0 right-0 h-0.5'} {classes?.indicator ?? ''}"
|
|
146
|
+
style="background:{accent};border-radius:1px;animation:karbon-tab-indicator 0.2s ease;"
|
|
147
|
+
></span>
|
|
45
148
|
{/if}
|
|
46
149
|
</button>
|
|
47
150
|
{/each}
|
|
48
151
|
</div>
|
|
49
152
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
</div>
|
|
56
|
-
{/if}
|
|
57
|
-
{/each}
|
|
153
|
+
<!-- Panel -->
|
|
154
|
+
{#if panel}
|
|
155
|
+
<div class="{vertical ? 'flex-1' : 'mt-4'} {classes?.panel ?? ''}" role="tabpanel" style="animation:karbon-tab-panel 0.2s ease;">
|
|
156
|
+
{@render panel(active)}
|
|
157
|
+
</div>
|
|
58
158
|
{/if}
|
|
59
159
|
</div>
|
|
160
|
+
|
|
161
|
+
<style>
|
|
162
|
+
@keyframes karbon-tab-indicator {
|
|
163
|
+
from { transform: scaleX(0); }
|
|
164
|
+
to { transform: scaleX(1); }
|
|
165
|
+
}
|
|
166
|
+
@keyframes karbon-tab-panel {
|
|
167
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
168
|
+
to { opacity: 1; transform: translateY(0); }
|
|
169
|
+
}
|
|
170
|
+
</style>
|
|
@@ -1,49 +1,139 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte'
|
|
3
|
-
import type {
|
|
3
|
+
import type { ButtonColor } from '@karbonjs/ui-core'
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
|
-
text
|
|
7
|
-
position?:
|
|
6
|
+
text?: string
|
|
7
|
+
position?: 'top' | 'bottom' | 'left' | 'right'
|
|
8
|
+
color?: ButtonColor
|
|
9
|
+
variant?: 'dark' | 'light' | 'colored'
|
|
10
|
+
size?: 'sm' | 'md' | 'lg'
|
|
11
|
+
delay?: number
|
|
12
|
+
arrow?: boolean
|
|
13
|
+
maxWidth?: string
|
|
14
|
+
nowrap?: boolean
|
|
15
|
+
content?: Snippet
|
|
8
16
|
class?: string
|
|
17
|
+
classes?: { root?: string, content?: string }
|
|
9
18
|
children: Snippet
|
|
10
19
|
}
|
|
11
20
|
|
|
12
21
|
let {
|
|
13
|
-
text,
|
|
22
|
+
text = '',
|
|
14
23
|
position = 'top',
|
|
24
|
+
color,
|
|
25
|
+
variant = 'dark',
|
|
26
|
+
size = 'md',
|
|
27
|
+
delay = 200,
|
|
28
|
+
arrow = true,
|
|
29
|
+
maxWidth = '250px',
|
|
30
|
+
nowrap = false,
|
|
31
|
+
content,
|
|
15
32
|
class: className = '',
|
|
33
|
+
classes = {},
|
|
16
34
|
children
|
|
17
35
|
}: Props = $props()
|
|
18
36
|
|
|
19
37
|
let visible = $state(false)
|
|
38
|
+
let timeout: ReturnType<typeof setTimeout> | null = null
|
|
20
39
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
function show() {
|
|
41
|
+
if (delay > 0) {
|
|
42
|
+
timeout = setTimeout(() => { visible = true }, delay)
|
|
43
|
+
} else {
|
|
44
|
+
visible = true
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hide() {
|
|
49
|
+
if (timeout) { clearTimeout(timeout); timeout = null }
|
|
50
|
+
visible = false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const sizeMap = {
|
|
54
|
+
sm: { px: '6px 10px', text: '11px', arrow: 4 },
|
|
55
|
+
md: { px: '8px 12px', text: '12px', arrow: 5 },
|
|
56
|
+
lg: { px: '10px 14px', text: '13px', arrow: 6 },
|
|
57
|
+
}
|
|
58
|
+
const s = $derived(sizeMap[size])
|
|
59
|
+
|
|
60
|
+
const accent = $derived(color ? `var(--karbon-${color}-500)` : '')
|
|
61
|
+
|
|
62
|
+
const bgColor = $derived.by(() => {
|
|
63
|
+
switch (variant) {
|
|
64
|
+
case 'dark': return 'rgba(15,10,30,0.95)'
|
|
65
|
+
case 'light': return 'rgba(255,255,255,0.97)'
|
|
66
|
+
case 'colored': return accent || 'var(--karbon-primary)'
|
|
67
|
+
default: return 'rgba(15,10,30,0.95)'
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const textColor = $derived.by(() => {
|
|
72
|
+
switch (variant) {
|
|
73
|
+
case 'dark': return 'rgba(255,255,255,0.92)'
|
|
74
|
+
case 'light': return 'rgba(15,10,30,0.85)'
|
|
75
|
+
case 'colored': return 'white'
|
|
76
|
+
default: return 'rgba(255,255,255,0.92)'
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// Position styles
|
|
81
|
+
const posStyle: Record<string, string> = {
|
|
82
|
+
top: 'bottom:100%;left:50%;transform:translateX(-50%);margin-bottom:8px;',
|
|
83
|
+
bottom: 'top:100%;left:50%;transform:translateX(-50%);margin-top:8px;',
|
|
84
|
+
left: 'right:100%;top:50%;transform:translateY(-50%);margin-right:8px;',
|
|
85
|
+
right: 'left:100%;top:50%;transform:translateY(-50%);margin-left:8px;',
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Arrow position
|
|
89
|
+
const arrowPos: Record<string, string> = {
|
|
90
|
+
top: 'top:100%;left:50%;transform:translateX(-50%);',
|
|
91
|
+
bottom: 'bottom:100%;left:50%;transform:translateX(-50%) rotate(180deg);',
|
|
92
|
+
left: 'left:100%;top:50%;transform:translateY(-50%) rotate(-90deg);',
|
|
93
|
+
right: 'right:100%;top:50%;transform:translateY(-50%) rotate(90deg);',
|
|
26
94
|
}
|
|
27
95
|
</script>
|
|
28
96
|
|
|
97
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
29
98
|
<div
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
onfocusout={() => visible = false}
|
|
99
|
+
class="relative inline-flex {classes?.root ?? className}"
|
|
100
|
+
onmouseenter={show}
|
|
101
|
+
onmouseleave={hide}
|
|
102
|
+
onfocusin={show}
|
|
103
|
+
onfocusout={hide}
|
|
36
104
|
>
|
|
37
105
|
{@render children()}
|
|
38
106
|
|
|
39
|
-
{#if visible}
|
|
107
|
+
{#if visible && (text || content)}
|
|
40
108
|
<div
|
|
41
|
-
class="absolute z-50
|
|
42
|
-
|
|
43
|
-
{posClasses[position]}"
|
|
109
|
+
class="absolute z-50 pointer-events-none {classes?.content ?? ''}"
|
|
110
|
+
style="{posStyle[position]}animation:karbon-tooltip-in 0.15s ease;"
|
|
44
111
|
role="tooltip"
|
|
45
112
|
>
|
|
46
|
-
|
|
113
|
+
<div
|
|
114
|
+
class="relative rounded-lg font-medium {nowrap ? 'whitespace-nowrap' : 'whitespace-normal'}"
|
|
115
|
+
style="padding:{s.px};font-size:{s.text};background:{bgColor};color:{textColor};max-width:{maxWidth};box-shadow:0 4px 14px rgba(0,0,0,0.25);backdrop-filter:blur(8px);"
|
|
116
|
+
>
|
|
117
|
+
{#if content}
|
|
118
|
+
{@render content()}
|
|
119
|
+
{:else}
|
|
120
|
+
{text}
|
|
121
|
+
{/if}
|
|
122
|
+
|
|
123
|
+
{#if arrow}
|
|
124
|
+
<div
|
|
125
|
+
class="absolute"
|
|
126
|
+
style="{arrowPos[position]}width:0;height:0;border-left:{s.arrow}px solid transparent;border-right:{s.arrow}px solid transparent;border-top:{s.arrow}px solid {bgColor};"
|
|
127
|
+
></div>
|
|
128
|
+
{/if}
|
|
129
|
+
</div>
|
|
47
130
|
</div>
|
|
48
131
|
{/if}
|
|
49
132
|
</div>
|
|
133
|
+
|
|
134
|
+
<style>
|
|
135
|
+
@keyframes karbon-tooltip-in {
|
|
136
|
+
from { opacity: 0; transform: translateX(-50%) translateY(4px); }
|
|
137
|
+
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
138
|
+
}
|
|
139
|
+
</style>
|