@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.
Files changed (40) hide show
  1. package/package.json +3 -2
  2. package/src/accordion/Accordion.svelte +198 -23
  3. package/src/alert/AlertMessage.svelte +114 -20
  4. package/src/avatar/Avatar.svelte +16 -2
  5. package/src/badge/Badge.svelte +99 -10
  6. package/src/breadcrumb/Breadcrumb.svelte +124 -13
  7. package/src/button/Button.svelte +106 -21
  8. package/src/button/ButtonBrand.svelte +229 -0
  9. package/src/carousel/Carousel.svelte +161 -28
  10. package/src/code/CodeBlock.svelte +323 -0
  11. package/src/data/DataTable.svelte +319 -8
  12. package/src/data/Pagination.svelte +168 -28
  13. package/src/divider/Divider.svelte +91 -10
  14. package/src/dropdown/Dropdown.svelte +171 -27
  15. package/src/editor/RichTextEditor.svelte +861 -107
  16. package/src/form/Checkbox.svelte +110 -18
  17. package/src/form/ColorPicker.svelte +28 -16
  18. package/src/form/DatePicker.svelte +20 -10
  19. package/src/form/{FormInput.svelte → Input.svelte} +41 -14
  20. package/src/form/Radio.svelte +86 -18
  21. package/src/form/Select.svelte +246 -33
  22. package/src/form/Slider.svelte +22 -7
  23. package/src/form/Textarea.svelte +53 -10
  24. package/src/form/Toggle.svelte +72 -18
  25. package/src/image/Image.svelte +6 -4
  26. package/src/image/ImageCompare.svelte +182 -0
  27. package/src/image/ImgZoom.svelte +131 -49
  28. package/src/index.ts +7 -1
  29. package/src/kbd/Kbd.svelte +4 -3
  30. package/src/layout/Card.svelte +12 -6
  31. package/src/layout/EmptyState.svelte +75 -8
  32. package/src/layout/PageHeader.svelte +111 -11
  33. package/src/overlay/Dialog.svelte +147 -67
  34. package/src/overlay/ImgBox.svelte +125 -21
  35. package/src/overlay/Modal.svelte +110 -28
  36. package/src/overlay/Toast.svelte +152 -55
  37. package/src/progress/Progress.svelte +137 -26
  38. package/src/skeleton/Skeleton.svelte +6 -4
  39. package/src/tabs/Tabs.svelte +133 -22
  40. package/src/tooltip/Tooltip.svelte +110 -20
@@ -1,59 +1,170 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte'
3
- import type { TabItem } from '@karbonjs/ui-core'
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
- children?: Snippet<[{ tab: TabItem }]>
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
- children
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, '&lt;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
- <div class="flex border-b border-[var(--karbon-border,rgba(0,0,0,0.07))]" role="tablist">
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={active === tab.id}
118
+ aria-selected={isActive}
34
119
  onclick={() => { if (!tab.disabled) select(tab.id) }}
35
120
  disabled={tab.disabled}
36
- class="px-4 py-2.5 text-sm font-medium transition-colors relative cursor-pointer
37
- disabled:opacity-40 disabled:cursor-not-allowed
38
- {active === tab.id
39
- ? 'text-[var(--karbon-primary)]'
40
- : 'text-[var(--karbon-text-3,#8e8aae)] hover:text-[var(--karbon-text,#1a1635)]'}"
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.label}
43
- {#if active === tab.id}
44
- <span class="absolute bottom-0 left-0 right-0 h-0.5 bg-[var(--karbon-primary)]"></span>
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
- {#if children}
51
- {#each tabs as tab}
52
- {#if active === tab.id}
53
- <div class="pt-4" role="tabpanel">
54
- {@render children({ tab })}
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 { TooltipPosition } from '@karbonjs/ui-core'
3
+ import type { ButtonColor } from '@karbonjs/ui-core'
4
4
 
5
5
  interface Props {
6
- text: string
7
- position?: TooltipPosition
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
- const posClasses: Record<string, string> = {
22
- top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
23
- bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
24
- left: 'right-full top-1/2 -translate-y-1/2 mr-2',
25
- right: 'left-full top-1/2 -translate-y-1/2 ml-2'
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
- role="group"
31
- class="relative inline-block {className}"
32
- onmouseenter={() => visible = true}
33
- onmouseleave={() => visible = false}
34
- onfocusin={() => visible = true}
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 px-2.5 py-1.5 rounded-lg text-xs font-medium whitespace-nowrap pointer-events-none
42
- bg-[var(--karbon-text,#1a1635)] text-white shadow-lg
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
- {text}
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>